一、无法被拒绝接受的async
好景不长C#5.0,句法糖小家庭又重新加入了三位新成员: async和await。
不过从我知道这两个混蛋后的极短一两年,我甚至都没搞知道如果怎么采用它,此种崭新的触发器程式设计商业模式对于生活习惯了现代商业模式的人而言毕竟是很多无法拒绝接受,代普雷有啥人依然在采用纯手工反弹委派的方式来展开触发器程式设计。
C#中的句法糖十分多,从手动特性到lock、using,觉得都较好认知很难就拒绝接受了,为何这回async和await就这么让人又爱又恨呢?
我想,不是即便它不太好用(恰好相反,认知了它后是十分新颖又功能强大的),而要即便它相比之下太晚了!
现代的触发器程式设计在各式各样词汇各式各样网络平台后端后端相差无几都是同一种商业模式,给触发器允诺传达一个反弹表达式,反弹表达式中再对积极响应展开处置,发动触发器允诺的地方对codice是不屑一顾的。我们早已生活习惯了这样的商业模式,即便此种商业模式十分老外。
而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 .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跟进看Start方法:
1 // System.Runtime.CompilerServices.AsyncVoidMethodBuilder 2 [__DynamicallyInvokable, DebuggerStepThrough] 3 public voidStart<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) { newProgram().SampleMethod(); Console.WriteLine(“THERE”); Console.Read(); }运行结果是这样的:
HERE THERE“THERE”被”HERE”阻塞了,并没有触发器先行。
虽然到此为止还没看到触发器发生,但是我们可以得出一个结论:
async不会导致触发器到底怎么才能触发器?还是得有多个线程才能触发器嘛,是时候引入Task了:async voidSampleMethod() { Task.Run(() => { Thread.Sleep(1000); Console.WriteLine(“HERE”); }); }Main方法不变,运行结果是这样的:
THERE HERE当然,把SampleMethod前头的async去掉也可以得到同样的结果。。。
所以async貌似是个鸡肋啊?不过并不是这样的!三、认知await,是谁在等
继续改造上面的SampleMethod,不过现在还得加一个GetHere的方法了:
async void SampleMethod() { Console.WriteLine(awaitGetHere()); }Task<string> GetHere() { return Task.Run(() => { Thread.Sleep(1000); return “HERE”; }); }Main方法依然不变,运行结果也没有变化。但是现在就不能去掉async了,即便没有async的方法里头不允许await!
首先要注意的是,GetHere方法的codice是Task
这一次的结论很难就得出了,很明显主线程没有等SampleMethod返回就继续往下走了,而调用WriteLine的线程则必须等到”HERE”返回才能转交到实参。
那么,WriteLine又是哪个线程调用的?
这一次可以轻车熟路间接找MoveNext方法了。需要注意的是,现在Program类里头已经变成了这副德性:这个时候try块里头的IL已经膨胀到了50行。。。还原为C#后如下:
1 bool <>t__doFinallyBodies; 2 Exception <>t__ex; 3 int CS$0$0000; 4TaskAwaiter<string> CS$0$0001;5 TaskAwaiter<string> CS$0$0002; 6 7 try 8 { 9 <>t__doFinallyBodies = true; 10CS$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 { 25CS$0$0001 = this.<>u__$awaiter1; 26 this.<>u__$awaiter1 = CS$0$0002; 27 this.<>1__state = –1; 28 } 29 30Console.WriteLine(CS$0$0001.GetResult());31 }貌似WriteLine依然是主线程调用的?!苦苦等待codice的难道还是主线程?!
四、触发器如何出现
觉得越看越奇怪了,既然主线程没有等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方法的codice是Task
这一次GetHere的返回又跑到”THERE”的前头了,即便没有await就没有阻塞,同时GetHere的本质也暴露了,codice确确实实就是个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 BodyFooter PaintEnd挺正常的结果,但是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 { 3Rendering(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 }这样的运行结果就算令人满意了:
PaintStart Header Paint End Ads Body Footer