程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> C# >> C#入門知識 >> [你必須知道的異步編程]C# 5.0 新特性——Async和Await使異步編程更簡單

[你必須知道的異步編程]C# 5.0 新特性——Async和Await使異步編程更簡單

編輯:C#入門知識

 

 在之前的C#基礎知識系列文章中只介紹了從C#1.0到C#4.0中主要的特性,然而.NET 4.5 的推出,對於C#又有了新特性的增加——就是C#5.0中async和await兩個關鍵字,這兩個關鍵字簡化了異步編程,之所以簡化了,還是因為編譯器給我們做了更多的工作,下面就具體看看編譯器到底在背後幫我們做了哪些復雜的工作的。

 對於同步的代碼,大家肯定都不陌生,因為我們平常寫的代碼大部分都是同步的,然而同步代碼卻存在一個很嚴重的問題,例如我們向一個Web服務器發出一個請求時,如果我們發出請求的代碼是同步實現的話,這時候我們的應用程序就會處於等待狀態,直到收回一個響應信息為止,然而在這個等待的狀態,對於用戶不能操作任何的UI界面以及也沒有任何的消息,如果我們試圖去操作界面時,此時我們就會看到"應用程序為響應"的信息(在應用程序的窗口旁),相信大家在平常使用桌面軟件或者訪問web的時候,肯定都遇到過這樣類似的情況的,對於這個,大家肯定會覺得看上去非常不舒服。引起這個原因正是因為代碼的實現是同步實現的,所以在沒有得到一個響應消息之前,界面就成了一個"卡死"狀態了,所以這對於用戶來說肯定是不可接受的,因為如果我要從服務器上下載一個很大的文件時,此時我們甚至不能對窗體進行關閉的操作的。為了具體說明同步代碼存在的問題(造成界面開始),下面通過一個程序讓大家更形象地看下問題所在:

          btnClick_Click(.btnClick.Enabled =  length =.btnClick.Enabled = 
.richTextBox1.Text += String.Format(=  = 
            HttpWebRequest webRequest = WebRequest.Create()  (webRequest != 
                 (WebResponse response = (Stream responseStream ==

運行程序後,當我們點擊窗體的 "點擊我"按鈕之後,在得到服務器響應之前,我們不能對窗體進行任何的操作,包括移動窗體,關閉窗體等,具體運行結果如下:

 上面部分我們已經看到同步方法所帶來的實際問題了,為了解決類似的問題,.NET Framework很早就提供了對異步編程的支持,下面就用.NET 1.0中提出的異步編程模型(APM)來解決上面的問題,具體代碼如下(注釋的部分通過獲得GUI線程的同步上文對象,然後同步調用同步上下文對象的post方法把要調用的方法交給GUI線程去處理,因為控件本來就是由GUI線程創建的,然後由它自己執行訪問控件的操作就不存在跨線程的問題了,程序中使用的是調用RichTextBox控件的Invoke方式來異步回調訪問控件的方法,其實背後的原來和注釋部分是一樣的,調用RichTextBox控件的Invoke方法可以獲得創建RichTextBox控件的線程信息(也就是前一種方式的同步上下文),然後讓Invoke回調的方法在該線程上運行):

  btnClick_Click(= = = caller.BeginInvoke(GetResult, 
            

         
             ( i = ; i < ; i++ 
         =
             resultvalue =

          ShowState(== 
        

運行的結果為:

 上面部分演示了使用傳統的異步編程模型(APM)來解決同步代碼所存在的問題,然而在.NET 2.0,.NET 4.0和.NET 4.5中,微軟都有推出新的方式來解決同步代碼的問題,他們分別為基於事件的異步模式,基於任務的異步模式和提供async和await關鍵字來對異步編程支持。關於前兩種異步編程模式,在我前面的文章中都有介紹,大家可以查看相關文章進行詳細地了解,本部分就C# 5.0中的async和await這兩個關鍵字如何實現異步編程的問題來給大家介紹下。下面通過代碼來了解下如何使用async和await關鍵字來實現異步編程,並且大家也可以參看前面的博客來對比理解使用async和await是異步編程更簡單。

     btnClick_Click( length = 
.richTextBox1.Text += String.Format(=
          Task<>= 
            HttpWebRequest webRequest = WebRequest.Create()  (webRequest != 
                 (WebResponse response =  (Stream responseStream == .richTextBox1.Text += 

運行結果如下:

我們對比下上面使用async和await關鍵字來實現異步編程的代碼和在第二部分的同步代碼,有沒有發現使用async和await關鍵字的異步實現和同步代碼的實現異步方法的運行在GUI線程上,所以就不用像APM中那樣考慮跨線程訪問的問題了(因為通過委托的BeginInvoke方法來進行回調方法時,回調方法是在線程池線程上執行的)

對於按鈕點擊事件的代碼來說,編譯器生成的背後代碼卻是下面這樣的,完全和我們源碼中的兩個樣:

  btnClick_Click(<btnClick_Click><>4__this = ==<>t__builder =<>1__state = -<>t__builder.Start<<btnClick_Click>d__0>(

看到上面的代碼,作為程序員的我想說——編譯器你怎麼可以這樣呢?怎麼可以任意篡改我的代碼呢?這樣不是侵犯我的版權了嗎?你要改最起碼應該告訴我一聲吧,如果我的源碼看到它在編譯器中的實現是上面那樣的,我相信我的源碼會說——難道我中了世間上最惡毒的面目全非腳嗎? 好吧,為了讓大家更好地理清編譯器背後到底做了什麼事情,下面就順著上面的代碼摸瓜,我也來展示耍一套還我漂漂拳來幫助大家找到編譯器代碼和源碼的對應關系。我的分析思路為:

1、提出問題——我的click事件的源碼到哪裡去了呢?

  從編譯器代碼我們可以看到,前面的7句代碼都是對某個類進行賦值的操作,最真正起作用的就是最後Start方法的調用。這裡又產生了幾個疑問—— 有了這兩個疑問,我們就點擊<btnClick_Click>d__0(反射工具可以讓我們直接點擊查看)來看看它是什麼類型

  <btnClick_Click>
      <> Form1 <> AsyncVoidMethodBuilder <>  <> TaskAwaiter<> <>  <length> 
     <> CS$$ <>t__doFinallyBodies =  (.<> -                         // 從這裡可以看出,其實async和await關鍵字背後的實現原理是基於任務的異步編程模式(TAP)
CS$$ = .<>            (CS$$           
.<>1__state = .<>u__$awaiter2 = CS$$           
.<>t__builder.AwaitUnsafeOnCompleted<TaskAwaiter<>, Form1.<btnClick_Click>d__0>( CS$$,  <>t__doFinallyBodies =                        // 當任務完成時,不會執行下面的代碼,會直接執行Label_007A中代碼
            CS$$ = .<>.<>u__$awaiter2 =  TaskAwaiter<>           .<>1__state = -            // 下面代碼是在GUI線程上執行的
            CS$$ =  TaskAwaiter<> CS$$ = CS$$.<length>5__1 = CS$$        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();
        }
         (Exception <>.<>1__state = -.<>t__builder.SetException(<>.<>1__state = -.<> .<>

  如果你看過我的迭代器專題的話,相信你肯定可以聯想到該結構體就是一個迭代器的一個實現,其主要方法就是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”關鍵字會導致調用方法用線程池線程運行嗎?

關於更多async和await關鍵字的常問問題可以查看——Async/Await FAQ和中文翻譯——(譯)關於async與await的FAQ

  寫到這裡本專題的內容就介紹到這裡的,並且我也會把本專題的內容同步到之前的C#基礎知識系列文章索引,這樣我的C#特性系列也就完整了,並且該專題也是異步編程的最後一篇專題,在後面的專題將為大家實現一個類似迅雷的多任務多線程下載器,對於這個專題可能會用到並行編程的內容,所以接下面我為為大家分享下並行編程的內容。

 

的建議,因為對於剛使用await的人,經常會問“幫來看一下怎麼死鎖了,怎麼辦啊,要死了,怎麼解決?”,對於這樣的問題大家應該明白一點就是——使用async標識的異步方法的運行在GUI線程上(對於這點大家一定要明白,在我文章中的剖析部分也詳細介紹了原因,閱讀文章的人應該重點了解),所以就不用像APM中那樣考慮跨線程訪問的問題了

 

本專題所有源碼下載:ASyncAndAwaitTestProject.zip

 

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved