1. 程式人生 > >[C#]非同步程式設計: async和await(1)

[C#]非同步程式設計: async和await(1)

[C#]剖析非同步程式設計語法糖: async和await

一、難以被接受的async

自從C#5.0,語法糖大家庭又加入了兩位新成員: async和await。

然而從我知道這兩個傢伙之後的很長一段時間,我甚至都沒搞明白應該怎麼使用它們,這種全新的非同步程式設計模式對於習慣了傳統模式的人來說實在是有些難以接受,不難想象有多少人仍然在使用手工回撥委託的方式來進行非同步程式設計。
C#中的語法糖非常多,從自動屬性到lock、using,感覺都很好理解很容易就接受了,為什麼偏偏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

轉自:https://www.cnblogs.com/vd630/p/4591760.html

 

最後想說的是,看IL比看bytecode實在麻煩太多了,CSC對程式碼動的手腳比JavaC多太多了。。。然而非常值得高興的是,MS所做的這一切,都是為了讓我們寫的程式碼更簡潔易懂,我們需要做的,就是把這些語法糖好好地利用起來。