使用Async和Await進行非同步程式設計(C#版 適用於VS2015)

tkbSimplest發表於2015-09-27

你可以使用非同步程式設計來避免你的應用程式的效能瓶頸並且加強總體的響應。然而,用傳統的技術來寫非同步應用是複雜的,同時編寫,除錯和維護都很困難。

VS2012介紹了簡單的方法,那就是非同步程式設計,它在.Net Framework 4.5和Windows 執行時提供了非同步支援。編譯器做了開發者以前做的困難的工作,而且你的應用保持了類似於非同步程式碼的邏輯結構。結果,你輕易地就獲得了所有非同步程式設計的優勢。

 

非同步提升響應

非同步對於可能阻塞的活動是至關重要的。例如當你的應用訪問Web的時候,訪問web資源有時有點慢或者延時,如果這樣一個活動在同步程式中阻塞了,整個應用就必須等待。在非同步程式中,此應用可以繼續其他的工作,而不依賴於web資源直到這個可能阻塞的任務完成。

下表展示了非同步程式設計提升響應的典型領域。陳列的來自Framework 4.5 和the Windows Runtime 的APIs包含了支援async程式設計的方法。

應用領域 包含非同步方法的APIs
Web 訪問 HttpClientSyndicationClient
處理檔案 StorageFileStreamWriterStreamReaderXmlReader
處理圖片 MediaCaptureBitmapEncoderBitmapDecoder
WCF程式設計 Synchronous and Asynchronous Operations

 

 

 

 

 

對於訪問UI執行緒的應用,非同步被證明是特別有價值的,因為所有Ui相關的活動通常共享一個執行緒。如果同步應用的任何一個程式被阻塞了,那麼所有程式都被阻塞了。屆時你的應用停止了響應,你可能推斷它出錯了,然而它卻僅僅在等待。

當你使用非同步方法的時候,應用會繼續響應UI。你可以調整或者最小化視窗,或者如果你不想等待應用完成,那就關了它。

基於非同步的方法相當於在設計非同步操作時,向可供你選擇的選項增加了自動的傳輸裝置。那就是說,你以更少的付出卻獲得了所有傳統非同步程式設計的好處。

非同步方法更容易編寫

關鍵字async和await是非同步程式設計的核心。通過使用這兩個關鍵字,可以使用.NET Framework 或者Windows Runtime的資源來建立非同步方法,這就像建立同步方法一樣簡單。使用await和async定義的方法為非同步方法。

 

下面是一個非同步方法的例子。程式碼中的所有你都應該看著很熟悉。

// 簽名中需要注意的三件事:
//  -方法有一個async修飾符. 
//  - 返回值是 Task 或 Task<T>. 
//    這裡返回一個Task<int>,因為return語句返回int型別
//  - 方法名以 "Async"結尾。
async Task<int> AccessTheWebAsync()
{ 
    // 先要新增 System.Net.Http引用來宣告 client.
    HttpClient client = new HttpClient();

    // GetStringAsync 返回 Task<string>. 這意味著當你等待這個任務的時候,你將獲得一個字串(urlContents)。
    Task<string> getStringTask = client.GetStringAsync("http://msdn.microsoft.com");


    //這裡你可以處理任務,它不依賴來自GetStringAsync的字串
    DoIndependentWork();

    // await 操作符延緩了AccessTheWebAsync.
    //  - AccessTheWebAsync 直到 getStringTask完成才繼續執行。
    //  - 同時, 控制返回到 AccessTheWebAsync的呼叫者.
    //  - 當getStringTask完成時,控制恢復. 
    //  - await 操作符然後檢索來自 getStringTask的字串.
    string urlContents = await getStringTask;

    //  return 語句表明返回一個整數.
    return urlContents.Length;
}

如果AccessTheWebAsync在呼叫GetStringAsync 和等待完成之間沒有其他要處理的程式碼,可以用下面一條單句簡化程式碼。

string urlContents = await client.GetStringAsync();

下面總結了一些上面的非同步方法的例子的特點:

  • 方法簽名包括async修飾符
  • 非同步方法的命名,按照慣例,以“Async”字尾結尾
  • 返回型別只能是這三種:Task<TResult>,Task或void
  • 方法通常至少包括一個await表示式,await標記了一個點,這個點就是直到非同步操作完成後非同步方法才繼續執行。同時,方法是延遲的,控制返回到方法的呼叫者。

非同步方法中發生什麼?

理解非同步程式設計最重要的事情是 控制流如何從一個方法移動到另一個方法。下圖帶你理解這個過程。

對應數字序號的解釋如下:

 

  1. 事件控制程式碼呼叫並等待AccessTheWebAsync 非同步方法。
  2. AccessTheWebAsync 建立一個HttpClient例項,並呼叫GetStringAsync非同步方法來下載website的內容,儲存到字串。
  3. GetStringAsync發生了一些事情,延遲了方法的進度。也許必須等待站點下載或者一些其他的阻塞活動。為避免阻塞資源,GetStringAsync轉讓控制權給它的呼叫者AccessTheWebAsync 。
  4. 因為getStringTask還沒有被await,所以AccessTheWebAsync 可以不依賴GetStringAsync返回的最終結果繼續工作。這項工作是一個同步方法DoIndependentWork。
  5. DoIndependentWork是一個處理一些事情的同步方法,並且返回給它的呼叫者。
  6. AccessTheWebAsync 已經完成了它能做的事情,但getStringTask還沒有返回結果。AccessTheWebAsync 下一步想要計算並返回已經下載的字串的長度,但是該方法直到有字串時才能計算那個值。
    因此,AccessTheWebAsync 使用了一個await操作符來延遲它的進度,並且轉讓控制權給呼叫AccessTheWebAsync 的方法。AccessTheWebAsync 返回一個Task<int>給呼叫者。這個task代表一種產生一個整數結果的允諾,這個整數結果就是下載字串的長度。
    【注意:如果GetStringAsync(並且從而getStringTask)在AccessTheWebAsync等待它之前完成,那麼控制權保留在AccessTheWebAsync中。如果呼叫的非同步過程(getStringTask)已經完成了,延遲和後來返回到AccessTheWebAsync的代價將會被浪費,因而AccessTheWebAsync沒必要等待最終的結果了。】
    在呼叫者內部(這個例子中的事件控制程式碼),處理模式繼續執行。在等待非同步返回的結果之前,呼叫者可能處理其他的工作而不依賴來自AccessTheWebAsync的結果,或者呼叫者可能會立即等待。事件控制程式碼等待AccessTheWebAsync,AccessTheWebAsync等待GetStringAsync。
  7. GetStringAsync完成併產生了字串結果。這個字串結果可能沒有按你期望的那樣通過GetStringAsync的呼叫被返回。(記住步驟3中該方法已經返回了一個task。)相反,這個字串被儲存到代表這個方法完成的任務物件getStringTask中。Await操作符檢索來自getStringTask的結果。賦值語句將檢索的結果賦值給urlContent變數。
  8. 當AccessTheWebAsync獲得字串結果時,該方法可以計算這個字串的長度。然後AccessTheWebAsync的工作也完成了,等待中的事件控制程式碼可以恢復了。

API非同步方法

你可能想知道在哪裡找到支援非同步的像GetStringAsync的方法。.NET Framework 4.5包含了許多對async和await有效的成員。你可以通過“Async”字尾和Task或Task<TResult>的返回型別來識別這些成員。比如,System.IO.Stream類中,在同步方法CopyTo, Read和 Write的旁邊包含了很多像CopyToAsync, ReadAsync和WriteAsync的方法。

執行緒

async方法規定為非阻塞操作。當等待的task執行的時候,async方法的await表示式不會阻塞當前執行緒。相反,該表示式註冊當前方法的剩餘作為延續,並且返回控制權給async方法的呼叫者。

async和await關鍵字不會造成額外的執行緒被建立。async方法不會要求多執行緒是因為async方法沒有執行在它自己的執行緒上。該方法執行在當前的同步上下文上,只有該方法啟用的時候才會在該執行緒上使用時間。你可以使用Task.Run()來將CPU受限的工作移動到後臺執行緒中,但是後臺執行緒不會幫助處理僅僅等待結果變成可利用的。

非同步和等待  

可以檢視我的這篇部落格Async and Await 非同步和等待

返回型別和引數

請檢視我的這篇部落格Async and Await 非同步和等待

命名慣例

按照慣例,給方法新增async修飾符,給方法名追加“Async”字尾。

複雜案例

下面的程式碼來自WPF應用的MainWindow.xaml.cs。

 

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

// 新增 System.Net.Http的using指令和引用;
using System.Net.Http;

namespace AsyncFirstExample
{
    public partial class MainWindow : Window
    {
        // 給事件控制程式碼新增 async標記,為的是可在方法體內使用 await .
        private async void StartButton_Click(object sender, RoutedEventArgs e)
        {
            // 分別呼叫和await.
            //Task<int> getLengthTask = AccessTheWebAsync();
            //// 可以在這裡做一些獨立的工作.
            //int contentLength = await getLengthTask;

            int contentLength = await AccessTheWebAsync();

            resultsTextBox.Text +=
                String.Format("\r\nLength of the downloaded string: {0}.\r\n", contentLength);
        }

        // 簽名中需要注意的三件事:
        //  -方法有一個async修飾符. 
        //  - 返回值是 Task 或 Task<T>. 
        //    這裡返回一個Task<int>,因為return語句返回int型別
        //  - 方法名以 "Async"結尾。
        async Task<int> AccessTheWebAsync()
        { 
            // 先要新增 System.Net.Http引用來宣告 client.
            HttpClient client = new HttpClient();

            // GetStringAsync 返回 Task<string>. 這意味著當你等待這個任務的時候,你將獲得一個字串(urlContents)。
            Task<string> getStringTask = client.GetStringAsync("http://msdn.microsoft.com");


            //這裡你可以處理任務,它不依賴來自GetStringAsync的字串
            DoIndependentWork();

            // await 操作符延緩了AccessTheWebAsync.
            //  - AccessTheWebAsync 直到 getStringTask完成才繼續執行。
            //  - 同時, 控制返回到 AccessTheWebAsync的呼叫者.
            //  - 當getStringTask完成時,控制恢復. 
            //  - await 操作符然後檢索來自 getStringTask的字串.
            string urlContents = await getStringTask;

            //  return 語句表明返回一個整數.
            return urlContents.Length;
        }

        void DoIndependentWork()
        {
            resultsTextBox.Text += "Working . . . . . . .\r\n";
        }
    }
}

 


 

相關文章