C#5.0引入了两个新关键词,async和await,大大简化了异步方法编程。
微软在不同时期使用的不同的异步模式,有3种:
- 异步模式
- 基于T事件的异步模式
- 基于Task的异步模式(TAP,Task Asynchronous Programming)
TAP是利用async和await关键字实现的,而且是微软极力推崇的一种异步编程方式。
注意!async和await关键字只是编译器功能。编译器会用Task类创建代码。如果不使用这两个关键词,C#4.0的Task类同样可以实现相同的功能,只是易读性和便利性差一些。
认识async和await
使用async和await关键字编写异步代码,具有与同步代码相当的结构和简单性,并且摒弃了异步编程的复杂结构。
但是在理解上刚开始会很不习惯,而且会把一些情况想当然了。现在我们用一些示例慢慢开始理解它
一个简单的同步方法
首先先写一个同步方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| namespace AsyncandAwaitTest { internal class Program { static void Main(string[] args) { Console.WriteLine($"头部已执行,当前主线程Id为:{Environment.CurrentManagedThreadId}"); string result = SayHi("jack"); Console.WriteLine(result); Console.WriteLine($"尾部已执行,当前主线程Id为:{Environment.CurrentManagedThreadId}"); Console.ReadKey(); } static string SayHi(string name) { Task.Delay(2000).Wait(); Console.WriteLine($"SayHi执行,当前线程Id为:{Environment.CurrentManagedThreadId}"); return $"Hello,{name}"; } } }
|

SayHi方法在主线程中运行,主线程被阻塞。
同步方法异步化
示例方法放到任务内执行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| namespace AsyncandAwaitTest { internal class Program { static void Main(string[] args) { Console.WriteLine($"头部已执行,当前主线程Id为:{Environment.CurrentManagedThreadId}"); string result = SayHiAsync("jack").Result; Console.WriteLine(result); Console.WriteLine($"尾部已执行,当前主线程Id为:{Environment.CurrentManagedThreadId}"); Console.ReadKey(); } static Task<string> SayHiAsync(string name) { return Task.Run(() => { return SayHi(name); }); } static string SayHi(string name) { Task.Delay(2000).Wait(); Console.WriteLine($"SayHi执行,当前主线程Id为:{Environment.CurrentManagedThreadId}"); return $"Hello,{name}"; } } }
|
我们在Main方法中调用了Task<string>.Result ,调用Result的时候就会在Task内部使用Wait,主线程还是被阻塞。

注意SayHi执行的线程Id指的是SayHiAsync方法的线程Id,Task.Delay(2000).Wait();会占用别的线程执行
延续任务(ContinueWith)
为了避免阻塞主线程使用任务延续的方式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| namespace AsyncandAwaitTest { internal class Program { static void Main(string[] args) { Console.WriteLine($"头部已执行,当前主线程Id为:{Environment.CurrentManagedThreadId}"); Task<string> task = SayHiAsync("jack"); task.ContinueWith(t => { Console.WriteLine($"延续执行,当前线程Id为{Environment.CurrentManagedThreadId}"); string result = t.Result; Console.WriteLine(result); }); Console.WriteLine($"尾部已执行,当前主线程Id为:{Environment.CurrentManagedThreadId}"); Console.ReadKey(); } static Task<string> SayHiAsync(string name) { return Task.Run(() => { return SayHi(name); }); } static string SayHi(string name) { Task.Delay(2000).Wait(); Console.WriteLine($"SayHi执行,当前主线程Id为:{Environment.CurrentManagedThreadId}"); return $"Hello,{name}"; } } }
|
使用延续任务,延续任务内部的方法也是在另外一个线程中运行,可以用来在任务完成后处理返回值,而主线程一直不会阻塞。

WhenAny、WhenAll、ContinueWith都属于Task中的延续操作,都不会阻塞主线程
使用await、async构建异步方法调用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| namespace AsyncandAwaitTest { internal class Program { static void Main(string[] args) { Console.WriteLine($"头部已执行,当前主线程Id为:{Environment.CurrentManagedThreadId}"); CallerWithAsync("jack"); Console.WriteLine($"尾部已执行,当前主线程Id为:{Environment.CurrentManagedThreadId}"); Console.ReadKey(); } async static void CallerWithAsync(string name) { Console.WriteLine($"异步调用头部执行,当前线程Id为:{Environment.CurrentManagedThreadId}"); string result = await SayHiAsync(name); Console.WriteLine($"异步调用尾部执行,当前线程Id为:{Environment.CurrentManagedThreadId}"); Console.WriteLine(result); } static Task<string> SayHiAsync(string name) { return Task.Run(() => { return SayHi(name); }); } static string SayHi(string name) { Task.Delay(2000).Wait(); Console.WriteLine($"SayHi执行,当前主线程Id为:{Environment.CurrentManagedThreadId}"); return $"Hello,{name}"; } } }
|
执行结果如下

使用await关键字调用返回Task的异步方法SayHiAsync,而使用await关键字的方法之前必须用async修饰,在SayHiAsync方法完成前,下面的方法不会执行,但是主线程并没有被阻塞。
本质
编译器将await关键字后的所有代码放进了延续(ContinueWith)方法的代码块中来转换await关键词。注意异步调用尾部执行的线程也是3,所以说明异步方法中await后面的代码都在子线程中调用了。如果一个异步方法中有多个await,它们就会互相嵌套,但是使用多个await的例子很少。
异步方法一运行到await,马上返回到主线程,主线程继续运行,await后面的代码在子线程中处理。
解析async和await
异步async
使用async修饰符标记的方法称为异步方法,异步方法只可以具有以下返回类型:
- Task
- Task<TResult>
- void
- 从C#7.0开始,任何具有可访问的
GetAwaiter方法的类型。GetAwaiter 方法返回的对象必须实现 System.Runtime.CompilerServices.ICriticalNotifyCompletion 接口。
异步方法通常包含await运算符的一个或多个实例,但缺少await也不会导致编译器错误,如果异步方法未使用await运算符标记暂停点,那么这个异步方法会作为同步方法执行。不过只有async没有await的方法编译器会发出警告
等待await
await表达式只能在由async修饰符标记的封闭方法体、Lambda表达式或异步方法中出现。在其他位置,它会解释为标识符。
使用await运算符的任务只可用于返回Task、Task<TResult>和System.Threading.Tasks.ValueType<TResult>对象的方法。
异步方法同步运行,直至到达其第一个await表达式,此时await在方法的执行中插入挂起点,会将方法挂起直到所等待的任务完成,然后执行await后面的代码区域。
await表达式不会阻止正在执行它的线程。而是使编译器将剩下的异步方法注册为等待任务的延续任务(在上面的例子中,Console.WriteLine($"异步调用尾部执行,当前线程Id为:{Environment.CurrentManagedThreadId}");Console.WriteLine(result);被注册成为了SayHiAsync的延续任务)。控制权会返回给异步方法的调用方。任务完成时,它会调用其延续任务,异步方法的执行会在暂停的位置处恢复。
注意:
- 无法等待具有void返回类型的异步方法,并且无效返回方法的调用方捕获不到异步方法抛出的任何异常。
- 异步方法无法声明in、ref或out参数,但可以调用包含此类参数的方法。同样,异步方法无法通过引用返回值,但可以调用包含ref返回值的方法。
异步方法运行机理(控制流)
异步编程中最需要弄清的是控制流是如何从方法移动到方法的。
下列示例及说明引自使用 Async和 Await 的任务异步编程 (TAP) 模型 (C#) | Microsoft Docs,清晰说明了控制流程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| namespace ControlFlowOfAsync { internal class Program { static void Main(string[] args) { var result = GetUrlContentLengthAsync(); Console.WriteLine(result.Result); Console.ReadKey(); } async static Task<int> GetUrlContentLengthAsync() { HttpClient client = new HttpClient();
Task<string> getStringTask = client.GetStringAsync("https://docs.microsoft.com/dotnet");
DoIndependentWork();
string contents = await getStringTask;
return contents.Length;
}
static void DoIndependentWork() { Console.WriteLine("Working"); } } }
|
await后面带有返回值的Task<TResult>会直接取得Result,这是因为Task实现了GetWaiter方法

多个异步方法
在一个异步方法里,可以调用一个或多个异步方法,如何编写取决于异步方法之间的结果是否互相依赖。
顺序调用异步方法
使用await关键字可以调用每个异步方法,如果一个异步方法需要使用另一个异步方法的结果,await关键词就非常必要。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| namespace MultipleAsyncMethods { internal class MultAsyncTest { static void Main(string[] args) { Console.WriteLine("执行前……"); GetResultAsync(); Console.WriteLine("执行中……"); Console.ReadKey(); } async static void GetResultAsync() { var number1 = await GetResult(10); var number2 = GetResult(number1); Console.WriteLine($"结果分别为:{number1}和{number2.Result}"); }
static Task<int> GetResult(int number) { return Task.Run<int>(() => { Task.Delay(1000).Wait(); return number + 10; }); } } }
|
如果不添加await,number2就无法获取到
使用组合器
如果异步方法相互不依赖,则每个异步方法都不适用await,而是通过Task变量引用每个异步方法,然后使用WhenAll、WhenAny等Task的延续操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| namespace MultipleAsyncMethods { internal class MultAsyncTest { static void Main(string[] args) { Console.WriteLine("执行前……"); GetResultAsync(); Console.WriteLine("执行中……"); Console.ReadKey(); } async static void GetResultAsync() { var number1 = GetResult(10); var number2 = GetResult(20); await Task.WhenAll(number1, number2); Console.WriteLine($"结果分别为:{number1.Result}和{number2.Result}"); }
static Task<int> GetResult(int number) { return Task.Run<int>(() => { Task.Delay(1000).Wait(); return number + 10; }); } } }
|
异步方法的异常处理
异常处理
下面的示例中演示了捕获异常的错误方式和正确方式。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| namespace ExceptionHandlingTest { internal class ExceptionHandling { static void Main(string[] args) { HandleWrong(); HandlerError(); Console.ReadKey(); } static void HandleWrong() { try { var task = ThrowAfter(0, "Handle Error"); } catch (Exception ex) {
Console.WriteLine(ex.Message); } } static async void HandlerError() { try { await ThrowAfter(1000, "HandlerError"); } catch (Exception ex) { Console.WriteLine(ex.Message); } } static async Task ThrowAfter(int seconds,string message) { await Task.Delay(seconds); throw new Exception(message); } } }
|
执行结果如下

调用异步方法,如果只是简单放在try/catch中,将会捕获不到异常,这是因为HandleWrong方法在ThrowAfter抛出异常之前就已经执行完毕。
注意我们的异步方法ThrowAfter返回的是Task,如果返回void的话就不会捕获异常了。
异步方法的异常一个较好的捕获方式,就是使用await关键字,将其防在try/catch语句块中
多个异步方法异常处理
如果调用了多个异步方法,在第一个异步方法抛出异常,后续的方法将不会被调用,catch块内只会处理出现的第一个异常。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| namespace ExceptionHandlingTest { internal class ExceptionHandling { static void Main(string[] args) { HandlerError(); Console.ReadKey(); } static async void HandlerError() { try { await ThrowAfter(1000, "HandlerError"); await ThrowAfter(2000, "HandleError2"); } catch (Exception ex) { Console.WriteLine(ex.Message); } } static async Task ThrowAfter(int seconds,string message) { await Task.Delay(seconds); throw new Exception(message); } } }
|
所以正确的做法是使用Task.WhenAll,不管任务是否抛出异常都会等到所有任务完成。Task.WhenAll结束后,异常被catch语句捕捉到。
但是如果只是捕捉Exception,我们只能看到WhenAll方法的第一个发生异常的任务信息,不会抛出后续的异常任务。
所以要捕获所有任务的异常信息,需要对任务声明内部变量,在try块引用变量,在catch块访问变量,然后在使用IsFaulted属性检查任务状态,以确认它们是否出现错误,然后再进行处理。示例如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| namespace ExceptionHandlingTest { internal class ExceptionHandling { static void Main(string[] args) { HandlerError(); Console.ReadKey(); } static async void HandlerError() { Task? task1 = null; Task? task2 = null; try { task1 = ThrowAfter(1000, "HandleError-One-Error"); task2 = ThrowAfter(2000, "HandleError-Two-Error"); await Task.WhenAll(task1, task2); } catch (Exception ex) { if (task1.IsFaulted) { Console.WriteLine(task1.Exception.InnerException.Message); } if (task2.IsFaulted) { Console.WriteLine(task2.Exception.InnerException.Message); } } } static async Task ThrowAfter(int seconds,string message) { await Task.Delay(seconds); throw new Exception(message); } } }
|

使用AggregateException获取异步方法异常
AggregateException包含了等待中所有异常的列表,Task.Exception返回的就是AggregateException,可以轻松遍历处理所有异常信息。示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| namespace ExceptionHandlingTest { internal class ExceptionHandling { static void Main(string[] args) { HandlerError(); Console.ReadKey(); } static async void HandlerError() { Task? taskResult = null; try { Task task1 = ThrowAfter(1000, "HandleError-One-Error"); Task task2 = ThrowAfter(2000, "HandleError-Two-Error"); await (taskResult = Task.WhenAll(task1, task2)); } catch (Exception) { foreach (var ex in taskResult.Exception.InnerExceptions) { Console.WriteLine(ex.Message); } } } static async Task ThrowAfter(int seconds,string message) { await Task.Delay(seconds); throw new Exception(message); } } }
|

重要补充与建议
提高相应能力
Dot Net有很多异步API我们可以通过async、await构建调用提高响应能力,例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| namespace ControlFlowOfAsync { internal class Program { static void Main(string[] args) { Demo(); Console.ReadKey(); } async static void Demo() { HttpClient client = new HttpClient();
var getTaskResult = await client.GetStringAsync("https://docs.microsoft.com/dotnet");
Console.WriteLine(getTaskResult);
} } }
|
这些API都以Asyc结尾。
下面列表展示了包含异步API的一些类
| 应用程序功能 |
支持包含异步方法的类 |
| Web访问 |
HttpClient、SyndicationClient |
| 处理文件 |
StorageFile、StreamWriter、StreamReader、XmlReader |
| 使用图像处理 |
MediaCapture、BitmapEncoder、BitmapDecoder |
| WCF编程 |
同步和异步操作 |
| 套接字处理 |
Socket |
重要建议
- async方法需要在主体中具有await关键字,否则它们将永不暂停。同时C#编译器将生成一个警告,此代码将会以类似普通方法的方式进行编译和运行。请注意这会导致效率低下,因为由C#编译器为异步方法生成的状态机将不会完成任何任务。
- 应将Async作为后缀添加到所编写的每个异步方法名称中。这是dot Net中的惯例,以便更轻松区分同步和异步方法。
- async void 应仅用于事件处理程序。因为事件不具有返回类型(因此无法返回Task和Task<TResult>)。其他任何对async void 的使用都不遵循TAP模型,且可能存在一定难度。例如:async void 方法中引发的异常无法在该方法外部被捕获或十分难以测试async void方法。
以非阻止方式处理等待任务
将阻止当前线程作为等待任务完成的方法,可能导致死锁和已阻止的上下文线程,且可能需要更复杂的错误处理。下表提供了关于如何以非阻止方式处理等待任务的指南:
| 使用一下方式 |
而不是…… |
若要执行此操作 |
| await |
Task.Wait或Task.Result |
检索后台任务的结果 |
| await Task.WhenAny |
Task.WhenAny |
等待任何任务完成 |
| await Task.WhenAll |
Task.WhenAll |
等待所有任务完成 |
| await Task.Delay |
Thread.Sleep |
等待一段时间 |
异步编程准则
异步编程的准则是确定所需执行的操作是I/O-Bound还是CPU-Bound。因为这会极大影响代码性能,并可能导致某些构造的误用。
考虑两个问题:
- 你的代码是否会“等待”某些内容,例如数据库中的数据Web资源等?如果答案为“是”,则你的工作是I/O-Bound。
- 你的代码是否要执行开销巨大的计算?如果答案为“是”,则你的工作是CPU-Bound。
如果你的工作是I/O-Bound,请使用async和await(而不是使用Task.Run),不应使用任务并行库。
如果你的工作为CPU-Bound,并且你重视相应能力,请使用async和await,并在另一个线程上使用Task.Run生成工作。如果该工作同时适用于并发和并行,则应考虑使用任务并行库。
Async/Await根本原理
Async和Await异步编程的原理 - 天边彩云 - 博客园 (cnblogs.com)