Async/Await实践
If the operation the expression is awaiting hasn't completed yet, the asynchronous function will return immediately, and it'll then continue where it left off (in an appropriate thread) when the value becomes available. The natural flow of not executing the next statement until this one has completed is still maintained but without blocking.
如果表达式等待的操作还未完成,那么异步函数将立即返回;当操作可用时,异步函数将(在适当的线程上)回到离开的地方继续执行。此前“在这条语句完成之前不要执行下一条语句”的流程依然不变,只是不再阻塞。
一般流程
public static async Task<int> ProcessRecords()
{
List<Record> records = await FetchRecordsAsync().ConfigureAwait(continueOnCapturedContext: false);
// 记录处理
await SaveResultsAsync(records).ConfigureAwait(continueOnCapturedContext: false);
// 让调用者知道处理了多少条记录
return records.Count;
}
private static Task SaveResultsAsync(List<Record> records)
{
// do sth
}
private static Task<List<Record>> FetchRecordsAsync()
{
// do sth
}
异步方法扩展
创建异步方法时,通常应考虑提供4个重载。4个重载均具有相同的基本参数,但要提供不同的选项,以用于进度报告和取消操作。假设要开发一个异步方法,其逻辑与下列同步方法相同:
Employee LoadEmployeeById(string id)
根据TAP的约定,需提供下列重载的一个或全部:
Task<Employee> LoadEmployeeById(string id);
Task<Employee> LoadEmployeeById(string id, CancellationToken cancellationToken);
Task<Employee> LoadEmployeeById(string id, IProgress<int> progress);
Task<Employee> LoadEmployeeById(string id, CancellationToken cancellationToken, IProgress<int> progress);
应用场景
- 是CPU密集型(CPU占用率很高,TPL),还是I/O密集型(应用async);
- 所在的托管线程里,是阻塞的还是非阻塞的(应用async);
I/O密集型,需要大量的输入输出,涉及到磁盘、网络操作,比如读文件、写文件、传输文件、网络请求、数据库sql等。
CPU密集型,如果是耗时比较长的任务,则应该使用 Task.Factory.StartNew 并指定 TaskCreateOptions.LongRunning 选项来新建独立线程,而不使用线程池里的线程执行任务。
真假异步
一般微软.net基本类库提供的异步函数基本都是I/O密集型的任务。
如果类库不提供IO异步函数,我们可以自己转换,但是改造后的方法可能不是很理想的异步方法,很容易出现假异步。
真异步是没有专用线程等待的。
假异步只是换了一个地方阻塞,对于性能的可拓展并没有什么好处。
我们知道IO是很慢的,如果让一个线程去等待他,显然是错误的做法,这也是假异步的做法。我们需要的是系统把数据准备好了,然后再通知托管线程继续处理。在系统处理IO的时候,托管线程就可以想干啥就干啥,而不是傻等着。
基于同步的异步方法包装,把原有的同步方法或者准备创建的新的同步方法通过 Task.Run 改成异步方法。因为如果是为了提升性能而这么做的话是没有意义的,它不会提升程序性能,反而可能会引起性能问题。
但是如果是为了实现类似不阻塞 Winform 主线程的效果的话也是可以这么做的。
应该或需要公开的异步方法应更具有可扩展性(Scalability,增加工作量,为缩短工作时间等),不应该仅仅只为了实现负载转移(offloading,把工作转移到其他资源进行处理,为保持UI线程的响应性等)的目的而公开同步方法对应的异步方法。异步实现可以交给调用者,同步方法的调用者可以很容易地通过使用专门针对异步处理同步方法的功能来实现这些好处,例如 Task.Run。
自己实现一个真正的异步是很难的,.NET 库中提供的异步方法都是使用”标准P/Invoke异步I/O系统“实现的,这种真正的异步操作不会有其它线程的参与。
async位置
-
TaskMethod();
执行 TaskMethod 但不等待它,同步执行(见await立即返回)后就继续执行后续代码。 -
await TaskMethod();
异步执行TaskMethod,释放当前线程->await等待->TaskMethod执行完成->继续执行后续代码。 -
Task.Run(async () =>{
// do sth
await TaskMethod();
};
从线程池中拿一个新的线程并开始执行任务,await立即返回后执行后续代码。
异步实践
原则 | 描述 | 例外 |
---|---|---|
避免异步void | 最好使用异步Task,async void无法捕获异常 | 响应事件 |
异步所有方式 | 不要混淆阻塞和异步代码 | Console Main方法 |
配置上下文 | 尽可能的使用ConfigureAwait(false) | 方法中需要上下文 |
- 可异步方式
需要做... | 不要用 | 可以用 |
---|---|---|
检索后台任务的结果 | Task.Wait 或 Task.Result | await |
等待任意一个任务完成 | Task.WaitAny | await Task.WhenAny |
检索多个任务的结果 | Task.WaitAll | await Task.WhenAll |
等待一段时间 | Thread.Sleep | await Task.Delay |
- 一般异步问题
问题 | 解决方案 |
---|---|
创建任务以执行代码 (I/O密集型) |
Task.Run 或 TaskFactory.StartNew (不是 Task constructor 或 Task.Start) |
为操作或事件创建任务封装器 (CPU密集型) |
TaskFactory.FromAsync 或 TaskCompletionSource |
支持取消 | CancellationTokenSource 和 CancellationToken |
报告进度 | IProgress |
处理数据流 | TPL数据流或反应式扩展 |
同步访问共享资源 | SemaphoreSlim |
异步初始化资源 | AsyncLazy |
异步就绪生产者/消费者结构 | TPL Dataflow 或 AsyncCollection |
异步事件处理函数尽可能短,保留方法的可测试性。
private async void button1_Click(object sender, EventArgs e)
{
// 其他需要同步处理的
try{
await Button1ClickAsync();
}catch(Exception ex){}
// 或封装好task,省略try
TryApiMethod(() => Button1ClickAsync());
}
public async Task Button1ClickAsync()
{
// Do asynchronous work.
await Task.Delay(1000);
}
try task
/// <summary>
/// 执行多线程,并封装异常问题
/// </summary>
public async void TryApiMethod(Func<Task> action, [System.Runtime.CompilerServices.CallerMemberName] string methodName = null)
{
Console.WriteLine($"{DateTime.Now:yyyy-MM-dd hh:mm:ss ffff} 线程{Thread.CurrentThread.ManagedThreadId} 执行方法:{methodName}");
try
{
await action().ConfigureAwait(false);
}
catch (AggregateException ex)
{
string err = ex.GetAggregateExceptionMsg();
Console.WriteLine(err);
_logger.LogError(this.TraceMessage(err), ex);
}
catch (WebException ex)
{
_logger.LogError(this.TraceMessage(ex.Message), ex);
}
catch (ThreadInterruptedException ex)
{
_logger.LogError(this.TraceMessage(ex.Message), ex);
}
catch (TaskCanceledException ex)
{
_logger.LogError(this.TraceMessage(ex.Message), ex);
}
catch (OperationCanceledException ex)
{
_logger.LogError(this.TraceMessage(ex.Message), ex);
}
catch (Exception ex)
{
_logger.LogError(this.TraceMessage(ex.Message), ex);
await ShowError(ex.Message, ex).ConfigureAwait(false);
}
}
TryApiMethod(() => MethodAsync());
其中,
public Task<MessageDialogResult> ShowError(string msg, Exception ex = null)
{
string errorMessage = string.IsNullOrWhiteSpace(ex?.Message.ToString()) ? "无" : ex?.Message.ToString();
errorMessage = msg + " [详情:" + errorMessage + "]";
var cts = new CancellationTokenSource(5000);
var mdsetting = new MetroDialogSettings()
{
AffirmativeButtonText = "确定",
CancellationToken = cts.Token
};
Task<MessageDialogResult> rslt = null;
_metroWindow.Dispatcher.Invoke(() =>
{
rslt = _metroWindow.ShowMessageAsync("错误提示:", errorMessage, MessageDialogStyle.Affirmative, mdsetting);
});
return rslt;
}
public void SetMetroWindow(MetroWindow window)
{
_metroWindow = window;
}
ConfigureAwait demo
如果在需要上下文的方法中的await之后有代码,则不应使用ConfigureAwait。这些代码包括操作GUI元素、写数据绑定属性或依赖于GUI特定类型(如Dispatcher/CoreDispatcher)。
ConfigureAwait(false),后续继续运行在当前task的线程上,注意后面如果有UI线程,需要切换。
private async Task HandleClickAsync()
{
// Can use ConfigureAwait here.
await Task.Delay(1000).ConfigureAwait(continueOnCapturedContext: false);
}
private async void button1_Click(object sender, EventArgs e)
{
button1.Enabled = false;
try
{
// Can't use ConfigureAwait here.
await HandleClickAsync();
}
finally
{
// We are back on the original context for this method.
button1.Enabled = true;
}
}
异步转换为TPL
Task Parallel Library,任务并行库
一般耗时长的方法
可中止
可取消
可显示进度
TAP
基于任务的异步模式,Task-based Asynchronous Pattern,使用单一方法表示异步操作的开始和完成。
TAP适合简单的并行场景。
.NET Framework 4.0 引入,异步编程的推荐方法。
使用单个方法表示异步操作的开始和完成。
TAP的异步方法以Async结尾;若要将TAP方法添加到已包含带Async后缀的EAP方法名称的类中,可改用后缀TaskAsync。
TAP将公开TestMethodAsync。
EAP
.NET Framework 2.0 引入,新开发中不再推荐使用。
EAP适合清晰的控制有来有往的多端异步场景。
基于事件的异步模式, Event-based Asynchronous Pattern,包括OperationNameAsync/OperationNameCompleted的方法/事件,OperationNameCompletedEventArg/AsyncCompletedEventArgs。
例如:WebClient.DownloadStringAsync和WebClient.DownloadStringCompleted,DownloadStringCompletedEventArgs。
DemoClass_EAP eap = new DemoClass_EAP();
eap.TestMethodCompleted += Eap_TestMethodCompleted;
var token = Guid.NewGuid();
PrintThreadId("EAP starting......");
eap.TestMethodAsync(3000, token);
Thread.Sleep(1000);
//eap.CancelAsnyc(token);
EAP将公开TestMethodAsync和TestMethodCompleted。XxxAsync方法不返回Task类型。
using System.Collections.Specialized;
using System.ComponentModel;
public class DemoClass_EAP
{
// 混合字典,提高性能
private readonly HybridDictionary _userStateToLifetime = new HybridDictionary();
private delegate void WorkerEventHandler(int duration, AsyncOperation asyncOp);
private readonly SendOrPostCallback _onTestMethodCompletedDelegate;
public delegate void TestMethodCompletedEventHandler(object sender, TestMethodCompletedEventArgs e);
// 事件
public event TestMethodCompletedEventHandler TestMethodCompleted;
public DemoClass_EAP()
{
_onTestMethodCompletedDelegate = new SendOrPostCallback(OnTestMethodCompleted);
}
// 异步方法
public void TestMethodAsync(int duration, object userToken)
{
var asyncOp = AsyncOperationManager.CreateOperation(userToken);
lock (_userStateToLifetime.SyncRoot)
{
if (_userStateToLifetime.Contains(userToken))
{
throw new ArgumentException("parameter must be unique", "userToken");
}
_userStateToLifetime[userToken] = asyncOp;
}
WorkerEventHandler worker = new WorkerEventHandler(TestMethodWorker);
worker.BeginInvoke(duration, asyncOp, null, null);
}
public void CancelAsnyc(object userToken)
{
var asyncOp = _userStateToLifetime[userToken] as AsyncOperation;
if (asyncOp != null)
{
lock (_userStateToLifetime.SyncRoot)
{
_userStateToLifetime.Remove(userToken);
}
asyncOp.OperationCompleted();
}
}
// 具体方法
private void TestMethodWorker(int duration, AsyncOperation asyncOp)
{
// TODO
Thread.Sleep(duration);
var e = new TestMethodCompletedEventArgs()
{
Duration = duration,
Message = DateTime.Now.ToString(),
};
PostOperationCompleted(asyncOp, _onTestMethodCompletedDelegate, e);
}
private void OnTestMethodCompleted(object state)
{
if (state is TestMethodCompletedEventArgs e)
{
TestMethodCompleted?.Invoke(this, e);
}
}
/// <summary>
/// 封送调用委托。任务已完成,asyncOp释放
/// </summary>
/// <param name="asyncOp">异步操作对象</param>
/// <param name="callback">回调方法</param>
/// <param name="e">回调参数</param>
private void PostOperationCompleted(AsyncOperation asyncOp, SendOrPostCallback callback, EventArgs e)
{
if (_userStateToLifetime[asyncOp.UserSuppliedState] != null)
{
lock (_userStateToLifetime.SyncRoot)
{
_userStateToLifetime.Remove(asyncOp.UserSuppliedState);
}
asyncOp.PostOperationCompleted(_onTestMethodCompletedDelegate, e);
}
}
}
public class TestMethodCompletedEventArgs : EventArgs
{
public int Duration { get; set; }
public string Message { get; set; }
}
EAP -> TPL
public Task<string> TestMethodTaskAsync(int duration)
{
var tcs = new TaskCompletionSource<string>();
try
{
TestMethodAsync(duration, Guid.NewGuid());
tcs.SetResult(string.Format("Long time operation time was {0}.", duration.ToString()));
}
catch (Exception ex)
{
tcs.SetException(ex);
}
return tcs.Task;
}
APM
.NET Framework 1.0 引入,新开发中不再推荐使用。
APM适合控制求解复杂结构的返回值的场景。
异步编程模型(旧版异步方法), Asynchronous Programming Model,包括BeginOperationName/EndOperationName方法对和IAsyncResult对象。
例如:FileStream.BeginRead 和 FileStream.EndRead。
DemoClass_APM apm = new DemoClass_APM();
var ar = apm.BeginTestMethod(3000, null, null);
PrintThreadId("The main thread continues to execute......");
Thread.Sleep(1000);
APM将公开BeginTestMethod和EndTestMethod方法。
public class DemoClass_APM
{
delegate string AsyncMethodCaller(int duration);
private string _result;
public IAsyncResult BeginTestMethod(int duration, AsyncCallback ar, object state)
{
if (ar == null)
{
ar = new AsyncCallback(CallbackMethod);
}
if (state == null)
{
state = "Long time operation....Completed! with return value \"{0}\"";
}
_result = string.Empty;
AsyncMethodCaller caller = new AsyncMethodCaller(TestMethod);
return caller.BeginInvoke(duration, ar, state);
}
public string EndTestMethod(IAsyncResult asyncResult)
{
var result = (AsyncResult)asyncResult;
var caller = (AsyncMethodCaller)result.AsyncDelegate;
var obj = (string)asyncResult.AsyncState;
if (result.EndInvokeCalled)
{
return _result;
}
_result = caller.EndInvoke(asyncResult);
PrintThreadId(string.Format(obj, _result));
return _result;
}
private void CallbackMethod(IAsyncResult ar)
{
_result = EndTestMethod(ar);
PrintThreadId("CallbackMethod: " + _result);
}
private string TestMethod(int duration)
{
Thread.Sleep(duration);
return string.Format("Long time operation time was {0}.", duration.ToString());
}
}
APM -> TPL
public static Task<int> ReadTaskAync(this Stream stream, byte[] buffer, int offset, int count, object state)
{
var tcs = new TaskCompletionSource<int>();
stream.BeginRead(buffer, offset, count, ar =>
{
try { tcs.SetResult(stream.EndRead(ar)); }
catch (Exception exc) { tcs.SetException(exc); }
}, state);
return tcs.Task;
}
// Method1
public Task<string> TestMethodAsnyc(int duration)
{
return Task<string>.Factory.FromAsync(BeginTestMethod(duration), EndTestMethod);
}
// Method2
public Task<string> TestMethodAsnyc(int duration)
{
//return Task<string>.Factory.FromAsync(BeginTestMethod(duration), EndTestMethod);
var tcs = new TaskCompletionSource<string>();
BeginTestMethod(duration, ar =>
{
try
{
tcs.SetResult(EndTestMethod(ar));
}
catch (OperationCanceledException)
{
tcs.SetCanceled();
}
catch (Exception ex)
{
tcs.SetException(ex);
}
}, null);
return tcs.Task;
}
TPL -> APM
public static IAsyncResult AsAPM<T>(this Task<T> task, AsyncCallback callback, object state)
{
if (task == null)
{
throw new ArgumentNullException("task");
}
var tcs = new TaskCompletionSource<T>();
task.ContinueWith(t =>
{
if (t.IsFaulted)
{
tcs.SetException(t.Exception.InnerExceptions);
}
else if (t.IsCanceled)
{
tcs.SetCanceled();
}
else
{
tcs.SetResult(t.Result);
}
if (callback != null)
{
callback(tcs.Task);
}
}, TaskScheduler.Default);
return tcs.Task;
}
异步转同步
var rslt = AwaitByPushFrame(obj.NormaMethodAsync());
using System.Diagnostics.Contracts;
using System.Windows.Threading;
/// <summary>
/// 通过消息循环同步等待异步操作
/// </summary>
/// <typeparam name="TResult">异步方法返回类型</typeparam>
/// <param name="task">异步的带有返回值的任务</param>
/// <returns></returns>
/// <remarks>阻塞当前线程并创建一个新的消息循环</remarks>
public static TResult AwaitByPushFrame<TResult>(Task<TResult> task)
{
if (task == null)
{
throw new ArgumentNullException(nameof(task));
}
Contract.EndContractBlock();
var frame = new DispatcherFrame();
task.ContinueWith(t =>
{
frame.Continue = false;
});
Dispatcher.PushFrame(frame);
return task.Result;
}
``