1、难以被接受的asynchtml
自从C#5.0,语法糖你们庭又加入了两位新成员: async和await。
然而从我知道这两个家伙以后的很长一段时间,我甚至都没搞明白应该怎么使用它们,这种全新的异步编程模式对于习惯了传统模式的人来讲实在是有些难以接受,不难想象有多少人仍然在使用手工回调委托的方式来进行异步编程。
C#中的语法糖很是多,从自动属性到lock、using,感受都很好理解很容易就接受了,为何恰恰async和await就这么让人又爱又恨呢?
我想,不是由于它很差用(相反,理解了它们以后是很是实用又易用的),而是由于它来得太迟了!
传统的异步编程在各类语言各类平台前端后端差很少都是同一种模式,给异步请求传递一个回调函数,回调函数中再对响应进行处理,发起异步请求的地方对于返回值是一无所知的。咱们早就习惯了这样的模式,即便这种模式十分蹩脚。
而async和await则打破了请求发起与响应接收之间的壁垒,让整个处理的逻辑再也不跳过来跳过去,成为了彻底的线性流程!线性才是人脑最容易理解的模式!
广告时间:
[C#]async和await刨根问底
这篇随笔把本文未解决的问题都搞定了,而且对async和await的整体面貌作了最终总结,对调查过程没有兴趣但愿直接看结果的能够直接戳进去~
前端
2、理解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方法:async
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 }
继续跟进:ide
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貌似是个鸡肋啊?然而并非这样的!
3、理解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仍然是主线程调用的?!苦苦等待返回值的难道仍是主线程?!
4、异步如何出现
感受越看越奇怪了,既然主线程没有等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被调用以前。。。从调用堆栈也获得有用的信息,这个问题之后有空再深究吧。。。
5、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的地方了。
6、使用示例
扯了这么多,扯得这么乱,我本身都晕乎了。。。
到底该怎么用嘛,看示例吧:
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所作的这一切,都是为了让咱们写的代码更简洁易懂,咱们须要作的,就是把这些语法糖好好地利用起来。