引言
c#5.0中async和await兩個關鍵字,這兩個關鍵字簡化了異步編程,之所以簡化了,還是因為編譯器給我們做了更多的工作,下面就具體看看編譯器到底在背后幫我們做了哪些復雜的工作的。
同步代碼存在的問題
對于同步的代碼,大家肯定都不陌生,因為我們平常寫的代碼大部分都是同步的,然而同步代碼卻存在一個很嚴重的問題,例如我們向一個web服務器發出一個請求時,如果我們發出請求的代碼是同步實現的話,這時候我們的應用程序就會處于等待狀態,直到收回一個響應信息為止,然而在這個等待的狀態,對于用戶不能操作任何的ui界面以及也沒有任何的消息,如果我們試圖去操作界面時,此時我們就會看到”應用程序為響應”的信息(在應用程序的窗口旁),相信大家在平常使用桌面軟件或者訪問web的時候,肯定都遇到過這樣類似的情況的,對于這個,大家肯定會覺得看上去非常不舒服。引起這個原因正是因為代碼的實現是同步實現的,所以在沒有得到一個響應消息之前,界面就成了一個”卡死”狀態了,所以這對于用戶來說肯定是不可接受的,因為如果我要從服務器上下載一個很大的文件時,此時我們甚至不能對窗體進行關閉的操作的。為了具體說明同步代碼存在的問題(造成界面開始),下面通過一個程序讓大家更形象地看下問題所在:
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
|
// 單擊事件 private void btnclick_click( object sender, eventargs e) { this .btnclick.enabled = false ; long length = accessweb(); this .btnclick.enabled = true ; // 這里可以做一些不依賴回復的操作 otherwork(); this .richtextbox1.text += string .format( "\n 回復的字節長度為: {0}.\r\n" , length); txbmainthreadid.text = thread.currentthread.managedthreadid.tostring(); } private long accessweb() { memorystream content = new memorystream(); // 對msdn發起一個web請求 httpwebrequest webrequest = webrequest.create( "http://msdn.microsoft.com/zh-cn/" ) as httpwebrequest; if (webrequest != null ) { // 返回回復結果 using (webresponse response = webrequest.getresponse()) { using (stream responsestream = response.getresponsestream()) { responsestream.copyto(content); } } } txbasynmethodid.text = thread.currentthread.managedthreadid.tostring(); return content.length; } |
運行程序后,當我們點擊窗體的 “點擊我”按鈕之后,在得到服務器響應之前,我們不能對窗體進行任何的操作,包括移動窗體,關閉窗體等,具體運行結果如下:
傳統的異步編程來改善程序的響應
上面部分我們已經看到同步方法所帶來的實際問題了,為了解決類似的問題,.net framework很早就提供了對異步編程的支持,下面就用.net 1.0中提出的異步編程模型(apm)來解決上面的問題,具體代碼如下(注釋的部分通過獲得gui線程的同步上文對象,然后同步調用同步上下文對象的post方法把要調用的方法交給gui線程去處理,因為控件本來就是由gui線程創建的,然后由它自己執行訪問控件的操作就不存在跨線程的問題了,程序中使用的是調用richtextbox控件的invoke方式來異步回調訪問控件的方法,其實背后的原來和注釋部分是一樣的,調用richtextbox控件的invoke方法可以獲得創建richtextbox控件的線程信息(也就是前一種方式的同步上下文),然后讓invoke回調的方法在該線程上運行):
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
|
private void btnclick_click( object sender, eventargs e) { this .richtextbox1.clear(); btnclick.enabled = false ; asyncmethodcaller caller = new asyncmethodcaller(testmethod); iasyncresult result = caller.begininvoke(getresult, null ); //// 捕捉調用線程的同步上下文派生對象 //sc= synchronizationcontext.current; } # region 使用apm實現異步編程 // 同步方法 private string testmethod() { // 模擬做一些耗時的操作 // 實際項目中可能是讀取一個大文件或者從遠程服務器中獲取數據等。 for ( int i = 0; i < 10; i++) { thread.sleep(200); } return "點擊我按鈕事件完成" ; } // 回調方法 private void getresult(iasyncresult result) { asyncmethodcaller caller = (asyncmethodcaller)((asyncresult)result).asyncdelegate; // 調用endinvoke去等待異步調用完成并且獲得返回值 // 如果異步調用尚未完成,則 endinvoke 會一直阻止調用線程,直到異步調用完成 string resultvalue = caller.endinvoke(result); //sc.post(showstate,resultvalue); richtextbox1.invoke(showstatecallback, resultvalue); } // 顯示結果到richtextbox private void showstate( object result) { richtextbox1.text = result.tostring(); btnclick.enabled = true ; } // 顯示結果到richtextbox //private void showstate(string result) //{ // richtextbox1.text = result; // btnclick.enabled = true; //} #endregion |
運行的結果為:
c# 5.0 提供的async和await使異步編程更簡單
上面部分演示了使用傳統的異步編程模型(apm)來解決同步代碼所存在的問題,然而在.net 2.0,.net 4.0和.net 4.5中,微軟都有推出新的方式來解決同步代碼的問題,他們分別為基于事件的異步模式,基于任務的異步模式和提供async和await關鍵字來對異步編程支持。關于前兩種異步編程模式,在我前面的文章中都有介紹,大家可以查看相關文章進行詳細地了解,本部分就c# 5.0中的async和await這兩個關鍵字如何實現異步編程的問題來給大家介紹下。下面通過代碼來了解下如何使用async和await關鍵字來實現異步編程,并且大家也可以參看前面的博客來對比理解使用async和await是異步編程更簡單。
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
|
private async void btnclick_click( object sender, eventargs e) { long length = await accesswebasync(); // 這里可以做一些不依賴回復的操作 otherwork(); this .richtextbox1.text += string .format( "\n 回復的字節長度為: {0}.\r\n" , length); txbmainthreadid.text = thread.currentthread.managedthreadid.tostring(); } // 使用c# 5.0中提供的async 和await關鍵字來定義異步方法 // 從代碼中可以看出c#5.0 中定義異步方法就像定義同步方法一樣簡單。 // 使用async 和await定義異步方法不會創建新線程, // 它運行在現有線程上執行多個任務. // 此時不知道大家有沒有一個疑問的?在現有線程上(即ui線程上)運行一個耗時的操作時, // 為什么不會堵塞ui線程的呢? // 這個問題的答案就是 當編譯器看到await關鍵字時,線程會 private async task< long > accesswebasync() { memorystream content = new memorystream(); // 對msdn發起一個web請求 httpwebrequest webrequest = webrequest.create( "http://msdn.microsoft.com/zh-cn/" ) as httpwebrequest; if (webrequest != null ) { // 返回回復結果 using (webresponse response = await webrequest.getresponseasync()) { using (stream responsestream = response.getresponsestream()) { await responsestream.copytoasync(content); } } } txbasynmethodid.text = thread.currentthread.managedthreadid.tostring() ; return content.length; } private void otherwork() { this .richtextbox1.text += "\r\n等待服務器回復中.................\n" ; } |
運行結果如下:
async和await關鍵字剖析
我們對比下上面使用async和await關鍵字來實現異步編程的代碼和在第二部分的同步代碼,有沒有發現使用async和await關鍵字的異步實現和同步代碼的實現很像,只是異步實現中多了async和await關鍵字和調用的方法都多了async后綴而已。正是因為他們的實現很像,所以我在第四部分才命名為使用async和await使異步編程更簡單,就像我們在寫同步代碼一樣,并且代碼的coding思路也是和同步代碼一樣,這樣就避免考慮在apm中委托的回調等復雜的問題,以及在eap中考慮各種事件的定義。
從代碼部分我們可以看出async和await的使用確實很簡單,我們就如在寫同步代碼一般,但是我很想知道編譯器到底給我們做了怎樣的處理的?并且從運行結果可以發現,運行異步方法的線程和gui線程的id是一樣的,也就是說異步方法的運行在gui線程上,所以就不用像apm中那樣考慮跨線程訪問的問題了(因為通過委托的begininvoke方法來進行回調方法時,回調方法是在線程池線程上執行的)。下面就用反射工具看看編譯器把我們的源碼編譯成什么樣子的:
1
2
3
4
5
6
7
8
9
10
11
|
// 編譯器為按鈕click事件生成的代碼 private void btnclick_click( object sender, eventargs e) { <btnclick_click>d__0 d__; d__.<>4__this = this ; d__.sender = sender; d__.e = e; d__.<>t__builder = asyncvoidmethodbuilder.create(); d__.<>1__state = -1; d__.<>t__builder.start<<btnclick_click>d__0>( ref d__); } |
看到上面的代碼,作為程序員的我想說——編譯器你怎么可以這樣呢?怎么可以任意篡改我的代碼呢?這樣不是侵犯我的版權了嗎?你要改最起碼應該告訴我一聲吧,如果我的源碼看到它在編譯器中的實現是上面那樣的,我相信我的源碼會說——難道我中了世間上最惡毒的面目全非腳嗎? 好吧,為了讓大家更好地理清編譯器背后到底做了什么事情,下面就順著上面的代碼摸瓜,我也來展示耍一套還我漂漂拳來幫助大家找到編譯器代碼和源碼的對應關系。我的分析思路為:
1、提出問題——我的click事件的源碼到哪里去了呢?
從編譯器代碼我們可以看到,前面的7句代碼都是對某個類進行賦值的操作,最真正起作用的就是最后start方法的調用。這里又產生了幾個疑問——d__0是什么類型? 該類型中的<>t__builder字段類型的start方法到底是做什么用的? 有了這兩個疑問,我們就點擊d__0(反射工具可以讓我們直接點擊查看)來看看它是什么類型
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
78
79
80
81
82
|
// <btnclick_click>d__0類型的定義,從下面代碼可以看出它是一個結構體 // 該類型是編譯器生成的一個嵌入類型 // 看到該類型的實現有沒有讓你聯想到什么? private struct <btnclick_click>d__0 : iasyncstatemachine { // fields public int <>1__state; public form1 <>4__this; public asyncvoidmethodbuilder <>t__builder; private object <>t__stack; private taskawaiter< long > <>u__$awaiter2; public long <length>5__1; public eventargs e; public object sender; // methods private void movenext() { try { taskawaiter< long > cs$0$0001; bool <>t__dofinallybodies = true ; switch ( this .<>1__state) { case -3: goto label_010e; case 0: break ; default : // 獲取用于等待task(任務)的等待者。你要知道某個任務是否完成,我們就需要一個等待者對象對該任務進行一個監控,所以微軟就定義了一個等待者對象的 // 從這里可以看出,其實async和await關鍵字背后的實現原理是基于任務的異步編程模式(tap) // 這里代碼是在線程池線程上運行的 cs$0$0001 = this .<>4__this.accesswebasync().getawaiter(); // 如果任務完成就調轉到label_007a部分的代碼 if (cs$0$0001.iscompleted) { goto label_007a; } // 設置狀態為0為了退出回調方法。 this .<>1__state = 0; this .<>u__$awaiter2 = cs$0$0001; // 這個代碼是做什么用的呢?讓我們帶著問題看下面的分析 this .<>t__builder.awaitunsafeoncompleted<taskawaiter< long >, form1.<btnclick_click>d__0>( ref cs$0$0001, ref this ); <>t__dofinallybodies = false ; // 返回到調用線程,即gui線程,這也是該方法不會堵塞gui線程的原因,不管任務是否完成都返回到gui線程 return ; } // 當任務完成時,不會執行下面的代碼,會直接執行label_007a中代碼 cs$0$0001 = this .<>u__$awaiter2; this .<>u__$awaiter2 = new taskawaiter< long >(); // 為了使再次回調movenext代碼 this .<>1__state = -1; label_007a: // 下面代碼是在gui線程上執行的 cs$0$0001 = new taskawaiter< long >(); long cs$0$0003 = cs$0$0001.getresult(); this .<length>5__1 = cs$0$0003; // 我們源碼中的代碼這里的 this .<>4__this.otherwork(); this .<>4__this.richtextbox1.text = this .<>4__this.richtextbox1.text + string .format( "\n 回復的字節長度為: {0}.\r\n" , this .<length>5__1); this .<>4__this.txbmainthreadid.text = thread.currentthread.managedthreadid.tostring(); } catch (exception <>t__ex) { this .<>1__state = -2; this .<>t__builder.setexception(<>t__ex); return ; } label_010e: this .<>1__state = -2; this .<>t__builder.setresult(); } [debuggerhidden] private void setstatemachine(iasyncstatemachine param0) { this .<>t__builder.setstatemachine(param0); } } |
如果你看過我的迭代器專題的話,相信你肯定可以聯想到該結構體就是一個迭代器的一個實現,其主要方法就是movenext方法。
從上面的代碼的注釋應該可以幫助我們解決在第一步提到的第一個問題,即<btnclick_click>d__0是什么類型,下面就分析下第二個問題,從<btnclick_click>d__0結構體的代碼中可以發現<>t__builder的類型是asyncvoidmethodbuilder類型,下面就看看它的start方法的解釋——運行關聯狀態機的生成器,即調用該方法就可以開始運行狀態機,運行狀態機指的就是執行movenext方法(movenext方法中有我們源碼中所有代碼,這樣就把編譯器生成的click方法與我們的源碼關聯起來了)。
從上面代碼注釋中可以發現,當該movenext被調用時會立即還回到gui線程中,同時也有這樣的疑問——剛開始調用movenext方法時,任務肯定是還沒有被完成的,但是我們輸出我們源碼中的代碼,必須等待任務完成(因為任務完成才能調轉到label_007a中的代碼),此時我們應該需要回調movenext方法來檢查任務是否完成,(就如迭代器中的,我們需要使用foreach語句一直調用movenext方法),然而我們在代碼卻沒有找到回調的任何代碼啊?
對于這個疑問,回調movenext方法肯定是存在的,只是首次看上面代碼的朋友還沒有找到類似的語句而已,上面代碼注釋中我提到了一個問題——這個代碼是做什么用的呢?讓我們帶著問題看下面的分析,其實注釋下面的代碼就是起到回調movenext方法的作用,asyncvoidmethodbuilder.awaitunsafeoncompleted<tawaiter, tstatemachine> 方法就是調度狀態機去執行movenext方法,從而也就解決了回調movenext的疑問了。
相信大家從上面的解釋中可以找到源碼與編譯器代碼之間的對應關系了吧, 但是我在分析完上面的之后,又有一個疑問——當任務完成時,是如何退出movenext方法的呢?總不能讓其一直回調吧,從上面的代碼的注釋可以看出,當任務執行完成之后,會把<>1__state設置為0,當下次再回調movenext方法時就會直接退出方法,然而任務沒完成之前,同樣也會把<>1__state設置為0,但是switch部分后面的代碼又把<>1__state設置為-1,這樣就保證了在任務沒完成之前,movenext方法可以被重復回調,當任務完成之后,<>1__state設置為-1的代碼將不會執行,而是調轉到label_007a部分。
經過上面的分析之后,相信大家也可以耍出一套還我漂漂拳去分析異步方法accesswebasync(),其分析思路是和btnclick_click的分析思路是一樣的.這里就不重復啰嗦了。
分析完之后,下面再分享下幾個關于async和await常問的問題
問題一:是不是寫了async關鍵字的方法就代表該方法是異步方法,不會堵塞線程呢?
答: 不是的,對于只標識async關鍵字的(指在方法內沒有出現await關鍵字)的方法,調用線程會把該方法當成同步方法一樣執行,所以然而會堵塞gui線程,只有當async和await關鍵字同時出現,該方法才被轉換為異步方法處理。
問題二:“async”關鍵字會導致調用方法用線程池線程運行嗎?
答: 不會,被async關鍵字標識的方法不會影響方法是同步還是異步運行并完成,而是,它使方法可被分割成多個片段,其中一些片段可能異步運行,這樣這個方法可能異步完成。這些片段界限就出現在方法內部顯示使用”await”關鍵字的位置處。所以,如果在標記了”async”的方法中沒有顯示使用”await”,那么該方法只有一個片段,并且將以同步方式運行并完成。在await關鍵字出現的前面部分代碼和后面部分代碼都是同步執行的(即在調用線程上執行的,也就是gui線程,所以不存在跨線程訪問控件的問題),await關鍵處的代碼片段是在線程池線程上執行。總結為——使用async和await關鍵字實現的異步方法,此時的異步方法被分成了多個代碼片段去執行的,而不是像之前的異步編程模型(apm)和eap那樣,使用線程池線程去執行一整個方法。
以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持服務器之家。
原文鏈接:http://blog.csdn.net/u012391923/article/details/53521060