现代的异步.NET 程序使用两个关键字:async 和await。async 关键字加在方法声明上,它的主要目的是使方法内的await 关键字生效。如果async 方法有返回值,应返回Task<T>;如果没有返回值,应返回Task。这些task 类型相当于future,用来在异步方法结束时通知主程序。和其他方法一样,async 方法在开始时以同步方式执行。在async 方法内部,await 关键字对它的参数执行一个异步等待。它首先检查操作是否已经完成,如果完成了,就继续运行(同步方式)。否则,它会暂停async 方法,并返回,留下一个未完成的task。一段时间后,操作完成,async 方法就恢复运行。
一个async 方法是由多个同步执行的程序块组成的,每个同步程序块之间由await 语句分隔。第一个同步程序块在调用这个方法的线程中运行,但其他同步程序块在哪里运行呢?情况比较复杂。
最常见的情况是,用await 语句等待一个任务完成,当该方法在await 处暂停时,就可以捕捉上下文(context)。如果当前SynchronizationContext 不为空,这个上下文就是当前SynchronizationContext。如果当前SynchronizationContext 为空,则这个上下文为当前TaskScheduler。该方法会在这个上下文中继续运行。一般来说,运行UI 线程时采用UI 上下文,处理ASP.NET 请求时采用ASP.NET 请求上下文,其他很多情况下则采用线程池上下文。
因此,在上面的代码中,每个同步程序块会试图在原始的上下文中恢复运行。如果在UI线程中调用DoSomethingAsync,这个方法的每个同步程序块都将在此UI 线程上运行。但是,如果在线程池线程中调用,每个同步程序块将在线程池线程上运行。
要避免这种错误行为, 可以在await 中使用ConfigureAwait 方法, 将参数continueOnCapturedContext 设为false。接下来的代码刚开始会在调用的线程里运行,在被await 暂停后,则会在线程池线程里继续运行。
不要用void 作为async 方法的返回类型! async 方法可以返回void,但是这仅限于编写事件处理程序。一个普通的async 方法如果没有返回值,要返回Task,而不是void。
在编写任务并行程序时,要格外留意下闭包(closure)捕获的变量。记住闭包捕获的是引用(不是值),因此可以在结束时以不明显地方式分享这些变量。
数据并行和任务并行都使用动态调整的分割器,把任务分割后分配给工作线程。线程池在需要的时候会增加线程数量。线程池线程使用工作窃取队列。
每个.NET 程序都有一个线程池,线程池维护着一定数量的工作线程,这些线程等待着执行分配下来的任务。线程池可以随时监测线程的数量。
1、暂停一段时间
Task 类有一个返回Task 对象的静态函数Delay,这个Task 对象会在指定的时间后完成。
using System; using System.Threading; using System.Threading.Tasks; namespace DemoTask { class Program { static void Main(string[] args) { TimeSpan t = new TimeSpan(0, 0, 1); DelayResult(100, t); Console.ReadLine(); } static async Task<int> DelayResult(int result, TimeSpan delay) { await Task.Delay(delay); Console.WriteLine($"threadId={Thread.CurrentThread.ManagedThreadId}"); return result; } } }
2、返回完成的任务
可以使用Task.FromResult 方法创建并返回一个新的Task<T> 对象,这个Task 对象是已经完成的,并有指定的值。
using System; using System.Threading; using System.Threading.Tasks; namespace DemoTask { class Program { static void Main(string[] args) { GetValueAsync(); Console.ReadLine(); } static Task<int> GetValueAsync() { return Task.FromResult(0); } } }
3、报告进度
使用IProgress<T> 和Progress<T> 类型,编写的async 方法需要有IProgress<T> 参数。
using System; using System.Threading; using System.Threading.Tasks; namespace DemoTask { class Program { static void Main(string[] args) { CallMethodAsync(); Console.ReadLine(); } static async Task MethodAsync(IProgress<double> progress) { double percent = 0; await Task.Run(() => { for(int i = 0; i < 10; i++) { percent = i; progress.Report(percent); Thread.Sleep(1000); } }); } static async Task CallMethodAsync() { var progress = new Progress<double>(); progress.ProgressChanged += (sender, args) => { Console.WriteLine($"进度:{args}"); }; await MethodAsync(progress); } } }
需要注意的是,IProgress<T>.Report 方法可以是异步的。这意味着真正报告进度之前,MethodAsync 方法会继续运行。基于这个原因,最好把T 定义为一个不可变类型,或者至少是值类型。如果T 是一个可变的引用类型,就必须在每次调用IProgress<T>.Report 时,创建一个单独的副本。
4、等待一组任务完成
Task.WhenAll 方法可以实现这个功能。这个方法的输入为若干个任务,当所有任务都完成时,返回一个完成的Task 对象。
using System; using System.Threading; using System.Threading.Tasks; namespace DemoTask { class Program { static void Main(string[] args) { FuncAsync(); Console.ReadLine(); } static async Task FuncAsync() { Task<int> task1 = Task.FromResult(1); Task<int> task2 = Task.FromResult(2); Task<int> task3 = Task.FromResult(3); int[] results = await Task.WhenAll(task1, task2, task3); foreach (var item in results) Console.WriteLine(item); } } }
如果有一个任务抛出异常,则Task.WhenAll 会出错,并把这个异常放在返回的Task 中。如果多个任务抛出异常,则这些异常都会放在返回的Task 中。但是,如果这个Task 在被await 调用,就只会抛出其中的一个异常。如果要得到每个异常,可以检查Task.WhenALl返回的Task 的Exception 属性。
using System; using System.Threading; using System.Threading.Tasks; namespace DemoTask { class Program { static void Main(string[] args) { ObserveOneExceptionAsync(); ObserveAllExceptionsAsync(); Console.ReadLine(); } static async Task ThrowNotImplementedExceptionAsync() { await Task.FromResult(0); throw new NotImplementedException("NotImplementedException"); } static async Task ThrowInvalidOperationExceptionAsync() { await Task.FromResult(0); throw new InvalidOperationException("InvalidOperationException"); } static async Task ObserveOneExceptionAsync() { var task1 = ThrowNotImplementedExceptionAsync(); var task2 = ThrowInvalidOperationExceptionAsync(); try { await Task.WhenAll(task1, task2); } catch (Exception e) { Console.WriteLine(e.Message); } Console.WriteLine("----------------"); } static async Task ObserveAllExceptionsAsync() { var task1 = ThrowNotImplementedExceptionAsync(); var task2 = ThrowInvalidOperationExceptionAsync(); Task task = Task.WhenAll(task1, task2); try { await task; } catch (Exception e) { AggregateException ae = task.Exception; foreach(var item in ae.InnerExceptions) Console.WriteLine(item.Message); } } } }
5、等待任意一个任务完成
使用Task.WhenAny 方法。该方法的参数是一批任务,当其中任意一个任务完成时就会返回。作为返回值的Task 对象,就是那个完成的任务。
using System; using System.Threading; using System.Threading.Tasks; namespace DemoTask { class Program { static void Main(string[] args) { RunAsync(); Console.ReadLine(); } static Task<int> Read() { return Task.FromResult(1); } static Task<int> Write() { return Task.FromResult(2); } static async Task RunAsync() { var t1 = Read(); var t2 = Write(); Task<int> task = await Task.WhenAny(t1, t2); Console.WriteLine(task.Result); } } }
Task.WhenAny 返回的task 对象永远不会以“故障”或“已取消”状态作为结束。该方法的运行结果总是一个Task 首先完成。如果这个任务完成时有异常,这个异常也不会传递给Task.WhenAny 返回的Task 对象。
第一个任务完成后,考虑是否要取消剩下的任务。如果其他任务没有被取消,也没有被继续await,那它们就处于被遗弃的状态。被遗弃的任务会继续运行直到完成,它们的结果会被忽略,抛出的任何异常也会被忽略。
using System; using System.Threading; using System.Threading.Tasks; namespace DemoTask { class Program { static void Main(string[] args) { ObserveOneExceptionAsync(); Console.ReadLine(); } static async Task ThrowNotImplementedExceptionAsync() { await Task.FromResult(1); throw new NotImplementedException("NotImplementedException"); } static async Task ThrowInvalidOperationExceptionAsync() { await Task.FromResult(2); throw new InvalidOperationException("InvalidOperationException"); } static async Task ObserveOneExceptionAsync() { var task1 = ThrowNotImplementedExceptionAsync(); var task2 = ThrowInvalidOperationExceptionAsync(); try { await Task.WhenAny(task1, task2); } catch (Exception e) { Console.WriteLine(e.Message); } Console.WriteLine("没返回异常"); } } }
6、任务完成时的处理
正在await 一批任务,希望在每个任务完成时对它做一些处理。另外,希望在任务一完成就立即进行处理,而不需要等待其他任务。
最简单的方案是通过引入更高级的async 方法来await 任务,并对结果进行处理,从而重新构建代码。
using System; using System.Threading; using System.Threading.Tasks; namespace DemoTask { class Program { static void Main(string[] args) { ProcessTasksAsync(); Console.ReadLine(); } static async Task<int> DelayAsync(int val) { await Task.Delay(TimeSpan.FromSeconds(val)); return val; } static async Task AwaitAndProcessAsync(Task<int> t) { var result = await t; Console.WriteLine(result + " " + DateTime.Now.Ticks / 1000 / 10000); } static async Task ProcessTasksAsync() { Task<int> t1 = DelayAsync(2); Task<int> t2 = DelayAsync(3); Task<int> t3 = DelayAsync(1); var tasks = new[] { t1, t2, t3 }; Task[] ts = new Task[3]; for (int i = 0; i < 3; i++) ts[i] = AwaitAndProcessAsync(tasks[i]); await Task.WhenAll(ts); } } }
7、避免上下文延续
在默认情况下,一个async 方法在被await 调用后恢复运行时,会在原来的上下文中运行。如果是UI 上下文,并且有大量的async 方法在UI 上下文中恢复,就会引起性能上的问题。
为了避免在上下文中恢复运行,可让await 调用ConfigureAwait 方法的返回值,参数continueOnCapturedContext 设为false。
8、处理async Task方法的异常
9、处理async void方法的异常