概述
异步这个概念在不同语境下有不同的解释,比如在一个单核CPU里开启两个线程执行两个函数,通常认为这种调用是异步的,但对于CPU来说它是单核不可能同时运行两个函数,不过是由系统调度在不同的时间分片中执行。一般来说,如果两个工作能同时进行,就认为是异步的。在编程中,它通常代表函数的调用可以在不执行完的情况下返回,必要时在完成时回调。
有一个概念常常被混淆,多线程和异步。很多人认为异步就是多线程的,但是多线程只是实现异步的其中一种方式,除此之外还有系统中断,定时器,甚至可以自己写一个状态机实现异步(C# 的异步实现类似状态机)。
不同的编程语言有不同异步编程方法,在C#语言中,常常使用async/await等关键字,和Task等类来实现异步编程。
C#异步编程用法
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
Main函数异步调用IntTask,打印"等三秒吧",随后返回到Main函数打印“等待中”,在task.Result取值时阻塞,三秒后IntTask返回(此时Task.Result被赋值)打印“result = 1”。看一下用法:
- async: 异步函数使用async关键字修饰
- await: 等待异步函数返回
- Task:异步函数有返回值,且返回值为int类型
上述只是一个极简的用法,忽略了大量的细节,可以建立一个初步的印象。
async/await和Task简介
async
用async修饰一个方法,表明这个方法可以异步执行,其返回值必须是void/Task/Task<T>(T是返回值类型)其中一个,方法内的语句至少包含一个await关键字,否则会被同步的方式执行。
await
await只能修饰(返回值是)Task类型变量,此时会返回Task.Result或void而不是Task本身,在上述示例中,Main没有被async修饰,不能使用await,其返回值就是Task<int>, 而IntTask调用Task.Delay就是直接返回void。await也只能在被async修饰的函数的语句中使用。
Task
源于基于任务的异步模式(Task-based Asynchronous Pattern,TAP),被作为异步函数的返回值。异步函数的返回值有三种:
- void:"fire and forget"(触发并忘记)不需要知道状态(是否完成),比如抛出异常、打印日志时可以使用
- Task:需要知道是否完成(或失败)等状态,但是不需要返回值
- Task<T>:在Task的基础上还想要返回值
其他
异步函数不能使用ref/out修饰参数
实现原理剖析
如果使用反汇编等手段,可以看到上述示例代码的编译:

在返回1之前,好像有什么“奇怪的东西”被调用,编译器又背着开发者偷偷干了什么呢?
实现原理示例
在微软的开发博客里有一个叫谢尔盖·杰普利亚科夫(Sergey Tepliakov)的毛子曾提到这部分,来看一下他的示例:
源码
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
|
这是他的源代码,这个类叫做StockPrices(股票价格),其核心业务是根据公司ID查询股票价格GetStockPriceForAsync,这是一个异步调用,首先它先异步调用InitializeMapIfNeededAsync对数据库进行初始化,初始化完成尝试从数据库中获取该公司的股票价格返回。
上述提到编译器偷偷自己生成了代码,如果手动实现大概是怎样的呢?来看谢尔盖给出的解:
手动实现
|
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 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 |
|
从类名GetStockPriceForAsync_StateMachine可以看到,他为这个异步调用生成了一个状态机来实现异步,先来看下成员变量:
- StockPrices: 原来那个“股票价格”类的引用
- _companyId: 调用方法时的参数公司ID
- _tcs:TaskCompletionSource 创建并完成该任务的来源。
- _initializeMapIfNeededTask:调用初始化数据的异步任务
- _state:状态枚举
- Task:直接就是_tcs.Task,即该任务创建并完成的来源
现在看来这段代码的逻辑就比较清楚了,在调用异步查询股票的接口时,创建了一个状态机并调用状态机的Start函数,第一次进入start函数时状态机的状态是Start状态,它给_initializeMapIfNeededTask赋值,把状态机状态流转到Step1,并让_initializeMapIfNeededTask执行结束末尾再次调用Start函数(ContinueWith)。
_initializeMapIfNeededTask任务在等待了42毫秒后(Task.Delay(42)),末尾时再次调用了Start函数,此时状态为Step1。首先检查了Task状态,符合要求调用_tcs.SetResult(其实是给Task的Result赋值),此时异步任务完成。
TaskCompletionSource
看官方文档给的定义:
表示未绑定到委托的 Task<TResult> 的制造者方,并通过 Task 属性提供对使用者方的访问
简单的示例:
|
1 2 3 4 5 6 7 8 9 10 11 12 |
|
看的出来这个类就是对Task的包装,方便创建分发给使用者的任务。其核心就是包装Task并方便外面设置其属性和状态
Task.ContinueWith
创建一个在目标 Task 完成时异步执行的延续任务
可以传入一个委托,在Task完成的末尾调用。这是一个典型的续体传递风格(continuation-pass style)。
续体传递风格
续体传递风格(continuation-pass style, CPS),来看维基百科的描述:
A function written in continuation-passing style takes an extra argument: an explicit "continuation"; i.e., a function of one argument. When the CPS function has computed its result value, it "returns" it by calling the continuation function with this value as the argument. That means that when invoking a CPS function, the calling function is required to supply a procedure to be invoked with the subroutine's "return" value. Expressing code in this form makes a number of things explicit which are implicit in direct style. These include: procedure returns, which become apparent as calls to a continuation; intermediate values, which are all given names; order of argument evaluation, which is made explicit; and tail calls, which simply call a procedure with the same continuation, unmodified, that was passed to the caller
大概的意思是,这种风格的函数比起普通的有一个额外的函数指针参数,调用结束(即将return)调用或者函数参数(替代直接return到调用者Caller)。还有一些其他细节,就不多说了,感兴趣自行翻译查看。
来看一个极简的例子:
int a = b + c + d;
这是一个链式运算,是有顺序的,在C++中,上述运算其实是:
int a = (b + c) + d; 先计算tmp = b + c(tmp是寄存器上一个临时的值,也称将亡值),然后计算 int a = tmp + c
使用续体传递来模拟这一过程:
|
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 |
|
上述代码应该很清楚了,稍微看下应该能看明白。
C#编译器的实现
|
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 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 |
|
比较一下C#编译器生成的状态机:
- __this:StockPrices“股票价格”类的引用
- companyId:公司ID参数
- __builder:AsyncTaskMethodBuilder类型的表示返回任务的异步方法生成器
- __state:状态索引
- __task1Awaiter:TaskAwaiter类型,提供等待异步任务完成的对象
上述成员有一些和之前手撸的状态机不太一样,等下面会介绍,先来这一套的逻辑:
首先创建了一个_GetStockPriceForAsync_d__1状态机_GetStockPriceFor_d__并初始化赋值,随后调用了这个状态机的__builder的Start函数并把该状态机作为引用参数传入。__builder.Start函数会调到该状态机的MoveNext函数(下面会介绍),这和手撸代码状态机Start函数调用类似。
MoveNext与Start函数的处理过程也类似:第一次进来__state == -1,__builder.AwaitUnsafeOnCompleted切换上下文执行InitializeLocalStoreIfNeededAsync异步任务,并指定在完成后切换到当前上下文调用该状态机的MoveNext函数,类似手撸代码的Task.ContinueWith。第二次进入时,执行到__builder.SetResult(result),异步任务基本完成。
上述描述也是忽略了一些细节,下面是调用的时序图,会更清楚些,有些不太清楚的点后面会详细介绍。

TaskAwaiter
来看下官方定义:
public readonly struct TaskAwaiter : System.Runtime.CompilerServices.ICriticalNotifyCompletion 提供等待异步任务完成的对象
结构:

可以看到,这个所谓“等待异步任务完成的对象”,主要是保证实现ICriticalNotifyCompletion的接口OnCompleted等。
AsyncTaskMethodBuilder<TAwaiter,TStateMachine>(TAwaiter, TStateMachine)
官方定义:

个人认为可以视为异步任务的“门面”,它负责启动状态机,传递一些中间状态,并在最终SetResult时表示它和其子例程的异步任务结束。其中有一个方法AwaitUnsafeOnCompleted,值得研究一下。
AsyncTaskMethodBuilder.AwaitUnsafeOnCompleted
这个方法在上述中一笔带过,被描述为类似Task.ContinueWith,确实如此,但执行过程相当复杂,在这里也只是简单介绍下过程。
AwaitUnsafeOnCompleted首先会调用GetCompletionAction,GetCompletionAction创建了一个保存了上下文 context = ExecuteContext.FastCapture()的MoveNextRunner,并返回了指向的MoveNextRunner.Run函数的委托。
接着调用参数awaiter的UnsafeOnCompleted(completionAction)函数,这里completionAction就是上述的那个委托,内部调用了成员Task.SetContinuationForAwait函数来初始化续体,SetContinuationForAwait又调用AddTaskContinuation把延续方法添加到Task中,当上述示例源码中的InitializeMapIfNeededAsync函数执行完调用Runner.Run:
|
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 |
|
((IAsyncStateMachine) stateMachine).MoveNext() 重新调用了状态机的MoveNext()
/**********************************************
在软件开发中,异步编程是一项重要的技能,尤其是在处理IO密集型操作,如网络请求、数据库交互、文件读写等场景。C#语言中的async和await关键字使得编写异步代码变得更加简洁和易读。本文将深入解析C#中的async和await,帮助您更好地理解它们的工作原理和用法。
一、异步编程的基本概念及其在C#中的实现
异步编程是一种编程范式,它允许程序在等待耗时的操作完成时继续执行其他任务。这样可以避免程序在等待操作完成时挂起,提高应用程序的响应性和性能。
C#中的异步编程主要通过async和await关键字来实现。async关键字用于声明异步方法,而await关键字用于等待异步操作完成。
二、async关键字的定义及其用法
async关键字是一个函数修饰符,用于声明一个异步方法。当一个方法被标记为async时,它返回一个Task对象,而不是直接返回结果。这意味着该方法会在调用时立即返回一个Task实例,而实际的操作会在一个单独的线程上异步执行。
public async Task<string> GetDataAsync()
{
// 模拟耗时操作
await Task.Delay(1000);
return "Data received";
}
在上面的例子中,GetDataAsync方法被标记为async,它返回一个Task。调用这个方法时,它会立即返回一个Task对象,而实际的等待操作会在后台线程中进行。
三、await关键字的定义及其用法
await关键字用于等待一个Task或async方法完成。当在async方法中使用await时,它会暂停当前方法的执行,直到等待的Task完成。一旦Task完成,方法会继续执行后续的代码。
public async Task<string> GetDataAsync()
{
string data = await GetDataFromServerAsync();
return data;
}
public async Task<string> GetDataFromServerAsync()
{
// 模拟耗时操作
await Task.Delay(1000);
return "Data from server";
}
在上面的例子中,GetDataAsync方法中使用了await来等待GetDataFromServerAsync方法的完成。这样,GetDataAsync方法在等待GetDataFromServerAsync方法完成时不会阻塞主线程,而是继续执行其他任务。
示例代码:使用async和await编写一个简单的异步程序
下面是一个使用async和await编写异步程序的示例:
using System;
using System.Threading.Tasks;
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Main thread is running...");
// 创建一个异步任务
var task = GetDataAsync();
// 主线程继续执行其他任务
Console.WriteLine("Main thread is doing other tasks...");
// 等待异步任务完成
task.Wait();
// 获取异步任务的结果
string data = task.Result;
Console.WriteLine("Data received: " + data);
}
public static async Task<string> GetDataAsync()
{
Console.WriteLine("GetDataAsync started...");
// 模拟耗时操作
await Task.Delay(1000);
Console.WriteLine("GetDataAsync completed...");
return "Data from server";
}
}
在这个示例中,我们创建了一个名为GetDataAsync的异步方法,它使用await来等待一个模拟的耗时操作。在Main方法中,我们创建了GetDataAsync任务的实例,并使用Wait方法来等待任务完成。最后,我们使用Result属性来获取任务的结果。
四、async和await的优点
代码简洁性:async和await使得异步代码的结构更接近同步代码,降低了异步编程的复杂性。
性能提升:通过异步执行,async和await可以减少线程阻塞和上下文切换,提高应用程序的响应性和性能。
更好的错误处理:可以使用try…catch语句来捕获Task中的异常,简化错误处理流程。
注意事项
使用await必须在async方法中:只能在标记为async的方法中使用await关键字。
不要在UI线程中使用async和await:长时间运行的任务应该在其他线程上执行,以避免阻塞UI线程,影响用户交互。
理解Task的生命周期:使用async和await时,需要理解Task的生命周期和状态,包括Wait、Result和WaitAsync等方法的使用。
五、C#下async和await中常见问题汇总
在使用C#中的async和await关键字时,开发者可能会遇到一些常见问题。以下是一些这些问题及其可能的解决方案:
1. 异步方法中的await调用
问题: 在异步方法中直接调用另一个async方法时,应该使用await吗?
解答: 是的,你应该在异步方法中使用await来调用另一个async方法。这样可以确保当前方法等待被调用的异步方法完成,并且能够利用await的优化,例如不会阻塞线程。
public async Task MyAsyncMethod()
{
string result = await MyOtherAsyncMethod();
// 使用result进行后续操作
}
public async Task<string> MyOtherAsyncMethod()
{
// 耗时操作
return "Hello, World!";
}
2. 同步方法中的await
问题: 如何在同步方法中使用await?
解答: 在同步方法中不能直接使用await,因为await只能在async方法中使用。如果你需要在同步方法中等待异步操作完成,可以使用Task.Wait()或Task.Result,但后者在异步流中不推荐使用,因为它可能会导致死锁。
public void MySyncMethod()
{
Task.Wait(MyAsyncMethod());
// 继续执行其他同步操作
}
public async Task MyAsyncMethod()
{
string result = await MyOtherAsyncMethod();
// 使用result进行后续操作
}
3. await和异常处理
问题: 如何使用try…catch捕获await操作的异常?
解答: await操作会抛出异常,你可以使用try…catch语句来捕获这些异常。
public async Task MyAsyncMethod()
{
try
{
string result = await MyOtherAsyncMethod();
// 使用result进行后续操作
}
catch (Exception ex)
{
// 处理异常
}
}
4. await和Task取消
问题: 如何在await操作中处理任务取消?
解答: 你可以使用CancellationToken来取消await操作。在你的async方法中传递一个CancellationToken参数,并在需要取消时设置CancellationTokenSource的Cancel方法。
public async Task MyAsyncMethod(CancellationToken cancellationToken)
{
if (cancellationToken.IsCancellationRequested)
{
// 处理取消请求
return;
}
string result = await MyOtherAsyncMethod();
// 使用result进行后续操作
}
5. await和ConfigureAwait
问题: await关键字有不同的配置选项,比如ConfigureAwait(false),它们有什么作用?
解答: ConfigureAwait(false)告诉await不要在原始任务继续执行的上下文中执行后续代码。这通常用于避免上下文切换,但可能会导致难以追踪的异常。默认情况下,await会使用原始的上下文。
public async Task MyAsyncMethod()
{
string result = await MyOtherAsyncMethod().ConfigureAwait(false);
// 使用result进行后续操作
}
6. async和await的性能影响
问题: async和await会对应用程序的性能产生什么影响?
解答: async和await主要目的是为了提高应用程序的响应性和用户体验。它们通过异步执行耗时的操作来减少阻塞。然而,异步编程可能会引入额外的开销,如上下文切换和任务调度。在性能敏感的场景中,你应该对使用async和await进行评估,并测量它们的实际影响。
7. async和await的正确使用
问题: 如何判断何时应该使用async和await?
解答: async和await最适合用于需要等待耗时操作完成的方法,特别是当这些操作是IO密集型或长时间运行的时候。它们使得代码更易于阅读和维护,但也应该避免在不必要的情况下使用,以避免不必要的复杂性。
七、最佳实践
1. 使用async和await处理所有异步操作: 尽可能使用async和await来处理所有类型的异步操作,包括IO操作、数据库交互、Web请求等。
2. 避免在UI线程中进行长时间操作: 在UI应用程序中,避免在UI线程中执行长时间运行的任务,以保持界面响应性。使用async和await可以将这些任务放到后台线程中。
3. 使用Task.Run() cautiously: 虽然Task.Run()可以在后台线程中运行任务,但它不是线程池线程,可能会导致线程资源消耗。只在必要时使用它,例如当任务需要自己的线程时。
4. 理解ConfigureAwait(false): 在大多数情况下,使用ConfigureAwait(false)是有益的,因为它可以减少上下文切换的开销。但是,如果你需要在原始上下文中继续执行代码,比如更新UI,那么你应该使用ConfigureAwait(true)。
5. 处理取消和异常: 总是检查异步方法是否可以被取消,并正确处理可能抛出的异常。使用CancellationToken来响应取消请求,并使用try…catch语句来处理异常。
6. 避免不必要的异步方法: 如果一个方法没有等待任何异步操作,或者它的所有操作都可以在同步中完成,那么不应该将其标记为async。
7. 使用await而不是Task.Result或Task.Wait(): await提供了更好的性能和异常处理,因此应优先使用。
总结
C#中的async和await是异步编程的便捷之选,它们使得编写异步代码变得更加简单和直观。通过深入理解async和await的原理和用法,您可以更好地利用异步编程的优势,提升应用程序的性能和用户体验。在实际开发中,合理运用async和await,可以让您更加高效地处理IO密集型任务。
async和await是C#中处理异步编程的强大工具,但它们的使用需要谨慎。正确使用async和await可以显著提高应用程序的性能和用户体验,而错误的使用则可能导致性能问题和代码复杂性增加。在设计异步逻辑时,应该考虑任务的性质、异常处理、任务取消、上下文切换等因素,以确保异步程序的稳定性和效率。
在实际开发中,建议对异步编程有深入的理解,并通过实际测试来评估async和await对应用程序性能的影响。通过不断学习和实践,开发者可以更好地掌握async和await,编写出既高效又易于维护的异步代码。



5141

被折叠的 条评论
为什么被折叠?



