2016-08-16 62 views
2

Microsoft說:「異步和await關鍵字不會導致創建額外的線程。異步方法不需要多線程,因爲異步方法不會在其自己的線程上運行。該方法在當前同步上下文上運行,並僅在該方法處於活動狀態時纔在線程上使用時間。您可以使用Task.Run將CPU綁定的工作移動到後臺線程,但後臺線程無助於只等待結果可用的進程。「C#控制流等待異步和線程

以下是Microsoft使用的Web請求示例用於解釋異步和等待的使用。 (https://msdn.microsoft.com/en-us/library/mt674880.aspx)。在問題結尾我粘貼了示例代碼的相關部分。

我的問題是,在每個「var byteArray = await client.GetByteArrayAsync(url);」語句之後,控件返回到CreateMultipleTasksAsync方法,然後調用另一個ProcessURLAsync方法。在三次下載被調用後,它開始等待第一個ProcessURLAsync方法完成。但是如果ProcessURLAsync沒有在單獨的線程中運行,它如何繼續執行DisplayResults方法?因爲如果它不在不同的線程上,在將控制權返回給CreateMultipleTasksAsync之後,它永遠無法完成。你能提供一個簡單的控制流程,以便我能理解嗎?

假設任務download3 = ProcessURLAsync(..)之前完成了第一client.GetByteArrayAsync方法,正是被稱爲第一DisplayResults什麼時候?

private async void startButton_Click(object sender, RoutedEventArgs e) 
    { 
     resultsTextBox.Clear(); 
     await CreateMultipleTasksAsync(); 
     resultsTextBox.Text += "\r\n\r\nControl returned to startButton_Click.\r\n"; 
    } 


    private async Task CreateMultipleTasksAsync() 
    { 
     // Declare an HttpClient object, and increase the buffer size. The 
     // default buffer size is 65,536. 
     HttpClient client = 
      new HttpClient() { MaxResponseContentBufferSize = 1000000 }; 

     // Create and start the tasks. As each task finishes, DisplayResults 
     // displays its length. 
     Task<int> download1 = 
      ProcessURLAsync("http://msdn.microsoft.com", client); 
     Task<int> download2 = 
      ProcessURLAsync("http://msdn.microsoft.com/en-us/library/hh156528(VS.110).aspx", client); 
     Task<int> download3 = 
      ProcessURLAsync("http://msdn.microsoft.com/en-us/library/67w7t67f.aspx", client); 

     // Await each task. 
     int length1 = await download1; 
     int length2 = await download2; 
     int length3 = await download3; 

     int total = length1 + length2 + length3; 

     // Display the total count for the downloaded websites. 
     resultsTextBox.Text += 
      string.Format("\r\n\r\nTotal bytes returned: {0}\r\n", total); 
    } 


    async Task<int> ProcessURLAsync(string url, HttpClient client) 
    { 
     var byteArray = await client.GetByteArrayAsync(url); 
     DisplayResults(url, byteArray); 
     return byteArray.Length; 
    } 


    private void DisplayResults(string url, byte[] content) 
    { 
     // Display the length of each website. The string format 
     // is designed to be used with a monospaced font, such as 
     // Lucida Console or Global Monospace. 
     var bytes = content.Length; 
     // Strip off the "http://". 
     var displayURL = url.Replace("http://", ""); 
     resultsTextBox.Text += string.Format("\n{0,-58} {1,8}", displayURL, bytes); 
    } 
} 
+0

我想想你可能會發現我的['async' intro](http://blog.stephencleary.com/2012/02/async-and-await.html)有幫助。 –

回答

6

它調用功能,而無需創建一個新的線程的方式是主要的「UI」線程不斷通過工作要做,加工項目的隊列陸續會在隊列中。您可能會聽到的一個常見術語是「消息泵」

當你做一個await,你是從UI線程中運行,一旦調用完成,以GetByteArrayAsync一個新的工作將在隊列中放,當它變成工作的打開將繼續的其餘代碼方法。

GetByteArrayAsync也不使用線程來完成它的工作,它要求操作系統完成工作並將數據加載到緩衝區,然後等待操作系統告訴它操作系統已完成加載緩衝區。當這個消息從操作系統進入時,一個新的項目進入我之前討論的隊列(稍後我會介紹),一旦它變成該項目輪到它將它從操作系統獲得的小緩衝區複製到一個更大的內部緩衝區並重復這個過程。一旦它獲得了文件的所有字節,它就會表明它已經完成了你的代碼,導致你的代碼將它延續到隊列中(我解釋了上一段的內容)。

我之所以說「有點兒」在談到GetByteArrayAsync投入項目到隊列時,實際上存在在你的程序不止一個隊列。有一個用於UI,一個用於「線程池」,另一個用於「I/O完成端口」(IOCP)。線程池和IOCP的會生成或在游泳池重用短暫的線程,所以這technicaly可以被稱爲創建一個線程,但一個線程可以在任何線程將要創建的池閒置。

你的代碼,是將使用「UI隊列」,代碼GetByteArrayAsync是最有可能使用的線程池隊列做的工作,操作系統使用的消息告訴GetByteArrayAsync數據可用在緩衝區中使用IOCP隊列。

通過在執行await的行上添加.ConfigureAwait(false),您可以更改代碼以從使用UI隊列切換到線程池隊列。

var byteArray = await client.GetByteArrayAsync(url).ConfigureAwait(false); 

此設定告訴await「而不是試圖用SynchronizationContext.Current(如果你是在UI線程的UI隊列)排隊的工作中使用的‘默認’SynchronizationContext(這是線程池隊列)

讓我們假設第一 「client.GetByteArray異步」 方法 「任務download3 = ProcessURLAsync(..)」 之前完成 然後,會是 「任務 download3 = ProcessURLAsync(..)」 或「DisplayResults 「那會是 援引?因爲據我所知,他們都會在你提到的 隊列中。

我將努力使從鼠標點擊恰好完成的一切事件的明確序列

  1. 你點擊屏幕
  2. 操作系統鼠標上使用一個線程從IOCP池在UI消息隊列中放置一條WM_LBUTTONDOWN消息。
  3. UI消息隊列最終得到該消息,並讓所有控件知道它。
  4. 命名startButtonButton控制接收消息的消息,看到鼠標被放置在其自身,當事件被觸發並調用它的Click事件處理程序
  5. Click事件處理程序調用startButton_Click
  6. startButton_Click電話CreateMultipleTasksAsync
  7. CreateMultipleTasksAsync電話ProcessURLAsync
  8. ProcessURLAsync調用client.GetByteArrayAsync(url)
  9. GetByteArrayAsync最終在內部做一個base.SendAsync(request, linkedCts.Token),
  10. SendAsyncSendAsync在內部做了一堆東西,最終導致它從操作系統發送請求從本機DLL下載文件。

到目前爲止,沒有發生任何「異步」,這只是所有正常的同步代碼。如果它是同步或異步,那麼到此爲止的所有內容的行爲完全相同。

  1. 一旦向OS發出請求,SendAsync返回當前處於「正在運行」狀態的Task
  2. 後來在文件中達到一個response = await sendTask.ConfigureAwait(false);
  3. await檢查任務的狀態,發現它仍在運行,從而導致在「運行」狀態有一個新的任務返回的功能外,還要求該任務在完成後運行一些附加代碼,但使用線程池來執行附加代碼(因爲它使用了.ConfigureAwait(false))。
  4. 重複此過程直到最終GetByteArrayAsync返回「Running」中的Task<byte[]>
  5. await看到返回的Task<byte[]>是在「運行」狀態,並導致在「運行」狀態的新Task<int>返回功能外,還詢問Task<byte[]>運行使用SynchronizationContext.Current一些額外的代碼(因爲你做了沒有指定.ConfigureAwait(false)),這將導致運行時添加的代碼放入我們上次在步驟3中看到的隊列。
  6. ProcessURLAsync返回一個Task<int>,它處於「正在運行」狀態,並且該任務存儲在變量download1
  7. 步驟7-15再次得到重複變量download2download3

注意:我們仍然在UI線程上,還沒有在這整個過程中,產生控制回消息泵。

  • await download1它看到的任務是在「運行」狀態,它要求任務使用SynchronizationContext.Current它,然後創建一個新的Task是在運行一些額外的代碼「運行「狀態並返回。
  • awaitCreateMultipleTasksAsync的結果表明任務處於「正在運行」狀態,並要求任務使用SynchronizationContext.Current運行一些附加代碼。因爲該函數是async void它只是將控制權返回給消息泵。
  • 消息泵處理隊列中的下一條消息。

  • 好吧,明白了嗎?現在我們繼續討論「完成工作」時會發生什麼

    一旦您在任何時候執行了第10步操作系統可能會使用IOCP發送消息來告訴代碼它已完成填寫緩衝區,那麼IOCP線程可能會複製數據或它的掩碼要求一個線程池線程來做到這一點(我看起來不夠深刻,看看哪個)。

    此過程不斷重複,直到下載完所有數據後,一旦完全下載「額外代碼」(代理)步驟12,要求將該任務發送到SynchronizationContext.Post,因爲它使用了默認的代理上下文將由線程池執行。在該代表結束時,它將原來的具有「正在運行」狀態的Task標記爲已完成狀態。

    一旦在步驟13中返回的Task<byte[]>,在第14步等待它確實是SynchronizationContext.Post,這代表將包含類似的代碼

    Delegate someDelegate() => 
    { 
        DisplayResults(url, byteArray); 
        SetResultOfProcessURLAsyncTask(byteArray.Length); 
    } 
    

    因爲你傳遞的背景是該委託投入獲取UI背景要由UI處理的消息隊列,當有機會時,UI線程將得到它。

    一旦ProcessURLAsyncdownload1完成,這將導致一個委託,它看起來有點像

    Delegate someDelegate() => 
    { 
        int length2 = await download2; 
    } 
    

    因爲你傳遞的背景是由處理的UI背景下,這個代表把在消息隊列中獲得用戶界面,UI線程會在有機會時得到它。一旦一個完成它確實排隊了一個委託,它看起來有點像

    Delegate someDelegate() => 
    { 
        int length3 = await download3; 
    } 
    

    因爲你傳遞了這個委託把在消息隊列中獲得的UI上下文的上下文由UI進行處理,用戶界面線程會在有機會時得到它。一旦協議達成,排隊的委託,看起來有點像

    Delegate someDelegate() => 
    { 
        int total = length1 + length2 + length3; 
    
        // Display the total count for the downloaded websites. 
        resultsTextBox.Text += 
         string.Format("\r\n\r\nTotal bytes returned: {0}\r\n", total); 
        SetTaskForCreateMultipleTasksAsyncDone(); 
    } 
    

    因爲你傳遞了這個委託把在消息隊列中獲得的UI上下文的上下文由UI進行處理,UI線程將當它有機會的時候得到它。一旦「SetTaskForCreateMultipleTasksAsyncDone」被調用時,排隊的委託,看起來像

    Delegate someDelegate() => 
    { 
        resultsTextBox.Text += "\r\n\r\nControl returned to startButton_Click.\r\n"; 
    } 
    

    和你的工作是最終完成。

    我做了一些主要的簡化,並做了一些白色的謊言,使它更容易理解,但這是發生什麼的基本問題。當一個Task完成它的工作時,它將使用它已經處理的線程來執行SynchronizationContext.Post,該帖子將把它放入任何上下文的隊列中,並由處理隊列的「泵」處理。

    +0

    假設「任務download3 = ProcessURLAsync(..)」之前完成了第一個「client.GetByteArrayAsync」方法的話,會是「任務download3 = ProcessURLAsync(..)」或「DisplayResults」會被調用?因爲據我所知,他們都會在你提到的隊列中。 –

    +0

    @JohnL。我試圖一步一步地經歷發生的事情。讓我知道,如果有任何點你還沒有得到 –

    +0

    感謝這樣一個詳細和寶貴的解釋。因此,既然你說線程處理「有機會的時候」的消息,我可以把它看作:如果GetByteArrayAsync(或者任何異步方法)在調用函數到達await點之前完成(這是「await download1 「在這個例子中),然後在調用方法到達」await download1「之後,異步方法的其餘部分(DisplayResults就會被執行),以便線程處於空閒等待狀態。我是否正確? –

    0

    幫助我瞭解異步等待作品的方式是this restaurant metaphor by Eric Lippert。 在面試過程中的某個地方尋找異步等待。

    僅當您的線程有時需要等待很長時間才能完成,如將文件寫入磁盤,查詢數據庫中的數據,從互聯網獲取信息時,異步等待纔有意義。在等待這些操作完成時,您的線程可以自由地執行其他操作。

    在不使用異步等待的情況下,在冗長的處理之後執行其他操作並繼續原始代碼將非常麻煩並且難以理解和維護。

    這就是當異步等待來救援。使用異步等待您的線程不會等到冗長的過程完成。事實上,它記得在Task對象的長度處理之後仍然需要做某些事情,並開始做其他事情,直到它需要冗長過程的結果。

    在Eric Lippert的比喻中:開始烘烤麪包後,廚師不會等到線程啓動。相反,他開始煮雞蛋。

    在這段代碼看起來像:

    private async Task MyFunction(...) 
    { 
        // start reading some text 
        var readTextTask = myTextReader.ReadAsync(...) 
        // don't wait until the text is read, I can do other things: 
        DoSomethingElse(); 
        // now I need the result of the reading, so await for it: 
        int nrOfBytesRead = await readTextTask; 
        // use the read bytes 
        .... 
    } 
    

    會發生什麼事是你的線程進入ReadAsync功能。因爲函數是異步的,所以我們知道有一個地方在等待它。事實上,如果你沒有等待寫一個異步函數,你的編譯器會發出警告。你的線程執行ReadAsync中的所有代碼,直到達到await。而不是真的等待你的線程在其調用堆棧中上升,看看它是否可以做其他事情。在上面的例子中,它啓動DoSomethingElse()。

    過了一段時間你的線程看到await readTextTask。再次,而不是真的在等待它上漲,看看是否有一些代碼沒有等待。

    它繼續這樣做,直到每個人都在等待。然後,只有當你的線程真的不能再做任何事情時,它纔開始等待直到ReadAsync的等待完成。

    這種方法的優點是你的線程會等待較少,因而你的過程將過早結束。除此之外,它還可以讓您的調用者(包括UI)保持響應,而不會產生多線程的開銷和困難。

    您的代碼看起來是連續的,實際上它不是按順序執行的。每次等待時,都會執行調用堆棧中不等待的代碼。請注意,雖然它不是順序的,但它仍然全部由一個線程完成。

    請注意,這一切仍然是單線程。線程一次只能做一件事,所以當你的線程忙於做一些繁重的計算時,你的調用者不能做任何事情,並且你的程序在你的線程完成計算之前仍然不會響應。異步等待不會幫助你與THEAD

    這就是爲什麼你看到耗時的過程在一個單獨的線程啓動與使用Task.Run的awaitable任務。這將釋放你的線程來做其他事情。當然這個方法只有在你的線程在等待計算完成時還有別的事情要做時纔有意義,並且如果開始一個新線程的開銷比自己進行計算的成本更低。

    private async Task<string> ProcessFileAsync() 
    { 
        var calculationTask = Task.Run(() => HeavyCalcuations(...)); 
        var downloadTask = downloadAsync(...); 
    
        // await until both are finished: 
        await Task.WhenAll(new Task[] {calculationTask, downloadTak}); 
        double calculationResult = calculationTask.Result; 
        string downloadedText = downloadTask.Result; 
    
        return downloadedText + calculationResult.ToString(); 
    } 
    

    現在回到你的問題。

    某處在第一ProcessUrlAsync是AWAIT。而不是無所事事,你的線程將控制權交還給你的程序,並記住它還有一些處理任務對象downLoad1。它開始再次調用ProcessUrlAsync。不等待結果並開始第三次下載。每次記住它在Task對象downLoad2和downLoad3中還有某些事情要做。現在

    你真的過程無關了,所以它等待第一個下載完成。

    這並不意味着你的線程真正在做什麼,它上升它的調用堆棧,看看是否有任何來電者不是等待並開始處理。在你的例子中,Start_Button_Click正在等待,所以它轉到調用者,這可能是UI。 UI可能不在等待,所以可以自由地做其他事情。

    所有下載完成後,您的線程將繼續顯示結果。

    順便說一句,而不是等待三次,你可以等待所有任務使用Task.WhenAll

    await Task.WhenAll(new Task[] {downLoad1, download2, download3}); 
    

    另一個文件,幫了我很多理解異步的await完成是Async And Await by the ever so helpful Stephen Cleary