【转】剖析异步编程语法糖: async和await
一、难以被接受的async
自从C#5.0,语法糖大家庭又加入了两位新成员: async和await。
然而从我知道这两个家伙之后的很长一段时间,我甚至都没搞明白应该怎么使用它们,这种全新的异步编程模式对于习惯了传统模式的人来说实在是有些难以接受,不难想象有多少人仍然在使用手工回调委托的方式来进行异步编程。
C#中的语法糖非常多,从自动属性到lock、using,感觉都很好理解很容易就接受了,为什么偏偏async和await就这么让人又爱又恨呢?
我想,不是因为它不好用(相反,理解了它们之后是非常实用又易用的),而是因为它来得太迟了!
传统的异步编程在各种语言各种平台前端后端差不多都是同一种模式,给异步请求传递一个回调函数,回调函数中再对响应进行处理,发起异步请求的地方对于返回值是一无所知的。我们早就习惯了这样的模式,即使这种模式十分蹩脚。
而async和await则打破了请求发起与响应接收之间的壁垒,让整个处理的逻辑不再跳过来跳过去,成为了完全的线性流程!线性才是人脑最容易理解的模式!
广告时间:
[C#]async和await刨根问底
这篇随笔把本文未解决的问题都搞定了,并且对async和await的总体面貌做了最终总结,对调查过程没有兴趣希望直接看结果的可以直接戳进去~
二、理解async,谁被异步了
如果对于Java有一定认识,看到async的使用方法应该会觉得有些眼熟吧?
//Java
synchronized void sampleMethod() { }
// C#
async void SampleMethod() { }
说到这里我想对MS表示万分的感谢,幸好MS的设计师采用的简写而不是全拼,不然在没有IDE的时候(比如写上面这两个示例的时候)我不知道得检查多少次有没有拼错同步或者异步的单词。。。
Java中的synchronized关键字用于标识一个同步块,类似C#的lock,但是synchronized可以用于修饰整个方法块。
而C#中async的作用就是正好相反的了,它是用于标识一个异步方法。
同步块很好理解,多个线程不能同时进入这一区块,就是同步块。而异步块这个新东西就得重新理解一番了。
先看看async到底被编译成了什么吧:
1 .method private hidebysig
2 instance void SampleMethod () cil managed
3 {
4 .custom instance void [mscorlib]System.Runtime.CompilerServices.AsyncStateMachineAttribute::.ctor(class [mscorlib]System.Type) = (
5 01 00 1f 54 65 73 74 2e 50 72 6f 67 72 61 6d 2b
6 3c 53 61 6d 70 6c 65 4d 65 74 68 6f 64 3e 64 5f
7 5f 30 00 00
8 )
9 .custom instance void [mscorlib]System.Diagnostics.DebuggerStepThroughAttribute::.ctor() = (
10 01 00 00 00
11 )
12 // Method begins at RVA 0x20b0
13 // Code size 46 (0x2e)
14 .maxstack 2
15 .locals init (
16 [0] valuetype Test.Program/'<SampleMethod>d__0',
17 [1] valuetype [mscorlib]System.Runtime.CompilerServices.AsyncVoidMethodBuilder
18 )
19
20 IL_0000: ldloca.s 0
21 IL_0002: ldarg.0
22 IL_0003: stfld class Test.Program Test.Program/'<SampleMethod>d__0'::'<>4__this'
23 IL_0008: ldloca.s 0
24 IL_000a: call valuetype [mscorlib]System.Runtime.CompilerServices.AsyncVoidMethodBuilder [mscorlib]System.Runtime.CompilerServices.AsyncVoidMethodBuilder::Create()
25 IL_000f: stfld valuetype [mscorlib]System.Runtime.CompilerServices.AsyncVoidMethodBuilder Test.Program/'<SampleMethod>d__0'::'<>t__builder'
26 IL_0014: ldloca.s 0
27 IL_0016: ldc.i4.m1
28 IL_0017: stfld int32 Test.Program/'<SampleMethod>d__0'::'<>1__state'
29 IL_001c: ldloca.s 0
30 IL_001e: ldfld valuetype [mscorlib]System.Runtime.CompilerServices.AsyncVoidMethodBuilder Test.Program/'<SampleMethod>d__0'::'<>t__builder'
31 IL_0023: stloc.1
32 IL_0024: ldloca.s 1
33 IL_0026: ldloca.s 0
34 IL_0028: call instance void [mscorlib]System.Runtime.CompilerServices.AsyncVoidMethodBuilder::Start<valuetype Test.Program/'<SampleMethod>d__0'>(!!0&)
35 IL_002d: ret
36 } // end of method Program::SampleMethod
不管你们吓没吓到,反正我第一次看到是吓了一大跳。。。之前的空方法SampleMethod被编译成了这么一大段玩意。
另外还生成了一个名叫'<SampleMethod>d__0'的内部结构体,整个Program类的结构就像这样:
其他的暂时不管,先尝试把上面这段IL还原为C#代码:
1 void SampleMethod()
2 {
3 '<SampleMethod>d__0' local0;
4 AsyncVoidMethodBuilder local1;
5
6 local0.'<>4_this' = this;
7 local0.'<>t__builder' = AsyncVoidMethodBuilder.Create();
8 local0.'<>1_state' = -1;
9
10 local1 = local0.'<>t__builder';
11 local1.Start(ref local0);
12 }
跟进看Start方法:
1 // System.Runtime.CompilerServices.AsyncVoidMethodBuilder
2 [__DynamicallyInvokable, DebuggerStepThrough]
3 public void Start<TStateMachine>(ref TStateMachine stateMachine) where TStateMachine : IAsyncStateMachine
4 {
5 this.m_coreState.Start<TStateMachine>(ref stateMachine);
6 }
继续跟进:
1 // System.Runtime.CompilerServices.AsyncMethodBuilderCore
2 [DebuggerStepThrough, SecuritySafeCritical]
3 internal void Start<TStateMachine>(ref TStateMachine stateMachine) where TStateMachine : IAsyncStateMachine
4 {
5 if (stateMachine == null)
6 {
7 throw new ArgumentNullException("stateMachine");
8 }
9 Thread currentThread = Thread.CurrentThread;
10 ExecutionContextSwitcher executionContextSwitcher = default(ExecutionContextSwitcher);
11 RuntimeHelpers.PrepareConstrainedRegions();
12 try
13 {
14 ExecutionContext.EstablishCopyOnWriteScope(currentThread, false, ref executionContextSwitcher);
15 stateMachine.MoveNext();
16 }
17 finally
18 {
19 executionContextSwitcher.Undo(currentThread);
20 }
21 }
注意到上面黄底色的stateMachine就是自动生成的内部结构体'<SampleMethod>d__0',再看看自动生成的MoveNext方法,IL就省了吧,直接上C#代码:
1 void MoveNext()
2 {
3 bool local0;
4 Exception local1;
5
6 try
7 {
8 local0 = true;
9 }
10 catch (Exception e)
11 {
12 local1 = e;
13 this.'<>1__state' = -2;
14 this.'<>t__builder'.SetException(local1);
15 return;
16 }
17
18 this.'<>1__state' = -2;
19 this.'<>t__builder'.SetResult()
20 }
因为示例是返回void的空方法,所以啥也看不出来,如果在方法里头稍微加一点东西,比如这样:
async void SampleMethod()
{
Thread.Sleep(1000);
Console.WriteLine("HERE");
}
然后再看看SampleMethod的IL:
1 .method private hidebysig
2 instance void SampleMethod () cil managed
3 {
4 .custom instance void [mscorlib]System.Runtime.CompilerServices.AsyncStateMachineAttribute::.ctor(class [mscorlib]System.Type) = (
5 01 00 1f 54 65 73 74 2e 50 72 6f 67 72 61 6d 2b
6 3c 53 61 6d 70 6c 65 4d 65 74 68 6f 64 3e 64 5f
7 5f 30 00 00
8 )
9 .custom instance void [mscorlib]System.Diagnostics.DebuggerStepThroughAttribute::.ctor() = (
10 01 00 00 00
11 )
12 // Method begins at RVA 0x20bc
13 // Code size 46 (0x2e)
14 .maxstack 2
15 .locals init (
16 [0] valuetype Test.Program/'<SampleMethod>d__0',
17 [1] valuetype [mscorlib]System.Runtime.CompilerServices.AsyncVoidMethodBuilder
18 )
19
20 IL_0000: ldloca.s 0
21 IL_0002: ldarg.0
22 IL_0003: stfld class Test.Program Test.Program/'<SampleMethod>d__0'::'<>4__this'
23 IL_0008: ldloca.s 0
24 IL_000a: call valuetype [mscorlib]System.Runtime.CompilerServices.AsyncVoidMethodBuilder [mscorlib]System.Runtime.CompilerServices.AsyncVoidMethodBuilder::Create()
25 IL_000f: stfld valuetype [mscorlib]System.Runtime.CompilerServices.AsyncVoidMethodBuilder Test.Program/'<SampleMethod>d__0'::'<>t__builder'
26 IL_0014: ldloca.s 0
27 IL_0016: ldc.i4.m1
28 IL_0017: stfld int32 Test.Program/'<SampleMethod>d__0'::'<>1__state'
29 IL_001c: ldloca.s 0
30 IL_001e: ldfld valuetype [mscorlib]System.Runtime.CompilerServices.AsyncVoidMethodBuilder Test.Program/'<SampleMethod>d__0'::'<>t__builder'
31 IL_0023: stloc.1
32 IL_0024: ldloca.s 1
33 IL_0026: ldloca.s 0
34 IL_0028: call instance void [mscorlib]System.Runtime.CompilerServices.AsyncVoidMethodBuilder::Start<valuetype Test.Program/'<SampleMethod>d__0'>(!!0&)
35 IL_002d: ret
36 } // end of method Program::SampleMethod
看出来什么变化了吗?????看不出来就对了,因为啥都没变。
那追加的代码跑哪去了?!在这呢:
1 void MoveNext()
2 {
3 bool local0;
4 Exception local1;
5
6 try
7 {
8 local0 = true;
9 Thread.Sleep(1000);
10 Console.WriteLine("HERE");
11 }
12 catch (Exception e)
13 {
14 local1 = e;
15 this.'<>1__state' = -2;
16 this.'<>t__builder'.SetException(local1);
17 return;
18 }
19
20 this.'<>1__state' = -2;
21 this.'<>t__builder'.SetResult()
22 }
至今为止都没看到异步在哪发生,因为事实上一直到现在确实都是同步过程。Main方法里这么写:
static void Main(string[] args)
{
new Program().SampleMethod();
Console.WriteLine("THERE");
Console.Read();
}
运行结果是这样的:
HERE
THERE
"THERE"被"HERE"阻塞了,并没有异步先行。
虽然到此为止还没看到异步发生,但是我们可以得出一个结论:
async不会导致异步
到底怎么才能异步?还是得有多个线程才能异步嘛,是时候引入Task了:
async void SampleMethod()
{
Task.Run(() =>
{
Thread.Sleep(1000);
Console.WriteLine("HERE");
});
}
Main方法不变,运行结果是这样的:
THERE
HERE
当然,把SampleMethod前头的async去掉也可以得到同样的结果。。。
所以async貌似是个鸡肋啊?然而并不是这样的!
三、理解await,是谁在等
继续改造上面的SampleMethod,不过现在还得加一个GetHere的方法了:
async void SampleMethod()
{
Console.WriteLine(await GetHere());
} Task<string> GetHere()
{
return Task.Run(() =>
{
Thread.Sleep(1000);
return "HERE";
});
}
Main方法仍然不变,运行结果也没有变化。但是现在就不能去掉async了,因为没有async的方法里头不允许await!
首先要注意的是,GetHere方法的返回值是Task<string>,而从运行结果可以看出来WriteLine的重载版本是string参数,至于为什么,之后再看。
这一次的结论很容易就得出了,很明显主线程没有等SampleMethod返回就继续往下走了,而调用WriteLine的线程则必须等到"HERE"返回才能接收到实参。
那么,WriteLine又是哪个线程调用的?
这一次可以轻车熟路直接找MoveNext方法了。需要注意的是,现在Program类里头已经变成了这副德性:
这个时候try块里头的IL已经膨胀到了50行。。。还原为C#后如下:
1 bool '<>t__doFinallyBodies';
2 Exception '<>t__ex';
3 int CS$0$0000;
4 TaskAwaiter<string> CS$0$0001;
5 TaskAwaiter<string> CS$0$0002;
6
7 try
8 {
9 '<>t__doFinallyBodies' = true;
10 CS$0$0000 = this.'<>1__state';
11 if (CS$0$0000 != 0)
12 {
13 CS$0$0001 = this.'<>4__this'.GetHere().GetAwaiter();
14 if (!CS$0$0001.IsCompleted)
15 {
16 this.'<>1__state' = 0;
17 this.'<>u__$awaiter1' = CS$0$0001;
18 this.'<>t__builder'.AwaitUnsafeOnCompleted(ref CS$0$0001, ref this);
19 '<>t__doFinallyBodies' = false;
20 return;
21 }
22 }
23 else
24 {
25 CS$0$0001 = this.'<>u__$awaiter1';
26 this.'<>u__$awaiter1' = CS$0$0002;
27 this.'<>1__state' = -1;
28 }
29
30 Console.WriteLine(CS$0$0001.GetResult());
31 }
貌似WriteLine仍然是主线程调用的?!苦苦等待返回值的难道还是主线程?!
四、异步如何出现
感觉越看越奇怪了,既然主线程没有等SampleMethod返回,但是主线程又得等到GetResult返回,那么异步到底是怎么出现的呢?
注意到第20行的return,主线程跑进了这一行自然就直接返回了,从而不会发生阻塞。
那么新的问题又来了,既然MoveNext在第20行就直接return了,谁来再次调用MoveNext并走到第30行?
MoveNext方法是实现自IAsyncStateMachine接口,借助于ILSpy的代码解析,找到了三个调用方:
第一个是之前看到的,SampleMethod内部调用到的方法,后两个是接下来需要跟踪的目标。
调试模式跟到AsyncMethodBuilderCore的内部,然后在InvokeMoveNext和Run方法的首行打断点,设置命中条件为打印默认消息并继续执行。
最后在Main函数和lambda表达式的首行也打上同样的断点并设置打印消息。F5执行,然后可以在即时窗口中看到如下信息:
Function: Test.Program.Main(string[]), Thread: 0xE88 主线程
Function: Test.Program.GetHere.AnonymousMethod__3(), Thread: 0x37DC 工作线程
Function: System.Runtime.CompilerServices.AsyncMethodBuilderCore.MoveNextRunner.Run(), Thread: 0x37DC 工作线程
Function: System.Runtime.CompilerServices.AsyncMethodBuilderCore.MoveNextRunner.InvokeMoveNext(object), Thread: 0x37DC 工作线程
这样至少弄明白了一点,"HERE"是由另一个工作线程返回的。
看不明白的是,为什么lambda的执行在两次MoveNext被调用之前。。。从调用堆栈也得到有用的信息,这个问题以后有空再深究吧。。。
五、Task<TResult> to TResult
正如之前所说,GetHere方法的返回值是Task<string>,WriteLine接收的实参是string,这是怎么做到的呢?
关键当然就是调用GetHere时候用的await了,如果去掉await,就会看到这样的结果:
System.Threading.Tasks.Task`1[System.String]
THERE
这一次GetHere的返回又跑到"THERE"的前头了,因为没有await就没有阻塞,同时GetHere的本质也暴露了,返回值确确实实就是个Task。
这个时候再去看MoveNext里头的代码就会发现,try块里的代码再次变清净了。。。而这一次WriteLine的泛型参数就变成了object。
关键中的关键在于,这一个版本中不存在TaskAwaiter,也不存在TaskAwaiter.GetResult(详情参见上一段代码第30行)。
GetResult的实现如下:
1 // System.Runtime.CompilerServices.TaskAwaiter<TResult>
2 [__DynamicallyInvokable, TargetedPatchingOptOut("Performance critical to inline across NGen image boundaries")]
3 public TResult GetResult()
4 {
5 TaskAwaiter.ValidateEnd(this.m_task);
6 return this.m_task.ResultOnSuccess;
7 }
这就是Task<TResult>转变为TResult的地方了。
六、使用示例
扯了这么多,扯得这么乱,我自己都晕乎了。。。
到底该怎么用嘛,看示例吧:
1 void PagePaint()
2 {
3 Console.WriteLine("Paint Start");
4 Paint();
5 Console.WriteLine("Paint End");
6 }
7
8 void Paint()
9 {
10 Rendering("Header");
11 Rendering(RequestBody());
12 Rendering("Footer");
13 }
14
15 string RequestBody()
16 {
17 Thread.Sleep(1000);
18 return "Body";
19 }
假设有这么个页面布局的方法,依次对头部、主体和底部进行渲染,头部和底部是固定的内容,而主体需要额外请求。
这里用Sleep模拟网络延时,Rendering方法其实也就是对Console.WriteLine的简单封装而已。。。
PagePaint运行过后,结果是这样的:
Paint Start
Header
Body
Footer
Paint End
挺正常的结果,但是Header渲染完以后页面就阻塞了,这个时候用户没法对Header进行操作。
于是就进行这样的修正:
1 async void Paint()
2 {
3 Rendering("Header");
4 Rendering(await RequestBody());
5 Rendering("Footer");
6 }
7
8 async Task<string> RequestBody()
9 {
10 return await Task.Run(() =>
11 {
12 Thread.Sleep(1000);
13 return "Body";
14 });
15 }
运行结果变成了这样:
Paint Start
Header
Paint End
Body
Footer
这样就能在Header出现之后不阻塞主线程了。
不过呢,Footer一直都得等到Body渲染完成后才能被渲染,这个逻辑现在看来还没问题,因为底部要相对于主体进行布局。
然而我这时候又想给页面加一个广告,而且是fixed定位的那种,管啥头部主体想盖住就盖住,你们在哪它不管。
比如这样写:
1 async void Paint()
2 {
3 Rendering(await RequestAds());
4 Rendering("Header");
5 Rendering(await RequestBody());
6 Rendering("Footer");
7 }
出现了很严重的问题,头部都得等广告加载好了才能渲染,这样显然是不对的。
所以应该改成这样:
1 async void Paint()
2 {
3 PaintAds();
4 Rendering("Header");
5 Rendering(await RequestBody());
6 Rendering("Footer");
7 }
8
9 async void PaintAds()
10 {
11 string ads = await Task.Run(() =>
12 {
13 Thread.Sleep(1000);
14 return "Ads";
15 });
16 Rendering(ads);
17 }
这样的运行结果就算令人满意了:
Paint Start
Header
Paint End
Ads
Body
Footer
最后想说的是,看IL比看bytecode实在麻烦太多了,CSC对代码动的手脚比JavaC多太多了。。。然而非常值得高兴的是,MS所做的这一切,都是为了让我们写的代码更简洁易懂,我们需要做的,就是把这些语法糖好好地利用起来。
【转】剖析异步编程语法糖: async和await的更多相关文章
- [C#]剖析异步编程语法糖: async和await
一.难以被接受的async 自从C#5.0,语法糖大家庭又加入了两位新成员: async和await. 然而从我知道这两个家伙之后的很长一段时间,我甚至都没搞明白应该怎么使用它们,这种全新的异步编程模 ...
- C#基础系列——异步编程初探:async和await
前言:前面有篇从应用层面上面介绍了下多线程的几种用法,有博友就说到了async, await等新语法.确实,没有异步的多线程是单调的.乏味的,async和await是出现在C#5.0之后,它的出现给了 ...
- 多线程编程学习笔记——async和await(二)
接上文 多线程编程学习笔记——async和await(一) 三. 对连续的异步任务使用await操作符 本示例学习如何阅读有多个await方法方法时,程序的实际流程是怎么样的,理解await的异步 ...
- 多线程编程学习笔记——async和await(三)
接上文 多线程编程学习笔记——async和await(一) 接上文 多线程编程学习笔记——async和await(二) 五. 处理异步操作中的异常 本示例学习如何在异步函数中处理异常,学习如何对多 ...
- 【.NET异步编程系列1】:await&async语法糖让异步编程如鱼得水
前导 Asynchronous programming Model(APM)异步编程模型以BeginMethod(...) 和 EndMethod(...)结对出现. IAsyncResult Beg ...
- 异步编程新方式async/await
一.前言 实际上对async/await并不是很陌生,早在阮大大的ES6教程里面就接触到了,但是一直处于理解并不熟练使用的状态,于是决定重新学习并且总结一下,写了这篇博文.如果文中有错误的地方还请各位 ...
- 走进异步编程的世界--async/await项目使用实战
起因:今天要做一个定时器任务:五分钟查询一次数据库发现超时未支付的订单数据将其状态改为已经关闭(数据量大约100条的情况) 开始未使用异步: public void SelfCloseGpPayOrd ...
- 编程概念--使用async和await的异步编程
Asynchronous Programming with Async and Await You can avoid performance bottlenecks and enhance the ...
- 异步编程系列第05章 Await究竟做了什么?
p { display: block; margin: 3px 0 0 0; } --> 写在前面 在学异步,有位园友推荐了<async in C#5.0>,没找到中文版,恰巧也想提 ...
随机推荐
- MySQL中的decimal
MySQL DECIMAL数据类型用于在数据库中存储精确的数值.我们经常将DECIMAL数据类型用于保留准确精确度的列,例如会计系统中的货币数据. 要定义数据类型为DECIMAL的列,请使用以下语法: ...
- 『流畅的Python』第15章:上下文管理器和else块
- Django框架简介-模板系统
2.4 模板 官方文档 2.4.1 常用语法 只需要记两种特殊符号: {{ }}和 {% %} 变量相关的用{{}},逻辑相关的用{%%}. 2.4.1.1 变量 {{ 变量名 }} 变量名由字母数 ...
- XXX系统业务建模
1.识别业务参与者 参与者包括管理员.填报人员.审核人员和领导. 2.识别业务用例(用例图展现) 3.详述业务用例(填报需求这一用例,以活动图详细展现如下) 4.建立业务对象模型
- HTML5 FormData方法介绍
详细见链接 转载说明:转自CSDN上“诗渊”的博客
- "她等待刀尖已经太久"--茨维塔耶娃诗抄
生活 1 你无法夺走我的红晕—— 它强大——如同河水的汛潮! 你是猎人,可我不会上当, 你若追逐,我就会逃跑. 你无法夺走我鲜活的灵魂! 就这样,在急遽的追逐中—— 一匹阿拉伯的骏马, 微 ...
- laravel 实现增 与查
//调用模型层 <?phpnamespace App;use Illuminate\Support\Facades\DB;use Illuminate\Database\Eloquent\Mod ...
- SQL列转行用逗号隔开
declare @result varchar(255) set @result = ” select @result = @result + cast(F_IT_FWID as varchar( ...
- Ubuntu用android-ndk-r15c编译boost_1_65_1
方法一(最简单的): 下载:android-ndk-r16-beta1 然后下载Boost-for-Android:https://github.com/moritz-wundke/Boost-for ...
- php intval 两位小数乘以100后结果少1
价格处理的时候往往是两位小数需要换算成分,如:16.33元换算为1633分,直接乘以100也就行了的,但是又使用了一个转换为整数类型的函数intval() 这下子结果就不对了,如图: 结果: 可以 ...