c# 異步編程基礎講解

現代應用程序廣泛使用文件和網絡 I/O。I/O 相關 API 傳統上默認是阻塞的,導致用戶體驗和硬件利用率不佳,此類問題的學習和編碼的難度也較大。而今基於 Task 的異步 API 和語言級異步編程模式顛覆瞭傳統模式,使得異步編程非常簡單,幾乎沒有新的概念需要學習。

異步代碼有如下特點:

  • 在等待 I/O 請求返回的過程中,通過讓出線程來處理更多的服務器請求。
  • 通過在等待 I/O 請求時讓出線程進行 UI 交互,並將長期運行的工作過渡到其他 CPU,使用戶界面的響應性更強。
  • 許多較新的 .NET API 都是異步的。
  • 在 .NET 中編寫異步代碼很容易。

使用 .NET 基於 Task 的異步模型可以直接編寫 I/O 和 CPU 受限的異步代碼。該模型圍繞著Task和Task<T>類型以及 C# 的async和await關鍵字展開。本文將講解如何使用 .NET 異步編程及一些相關基礎知識。

Task 和 Task<T>

Task 是 Promise 模型的實現。簡單說,它給出“承諾”:會在稍後完成工作。而 .NET 的 Task 是為瞭簡化使用“承諾”而設計的 API。

Task 表示不返回值的操作, Task<T> 表示返回T類型的值的操作。

重要的是要把 Task 理解為發起異步工作的抽象,而不是對線程的抽象。默認情況下,Task 在當前線程上執行,並酌情將工作委托給操作系統。可以選擇通過Task.RunAPI 明確要求任務在單獨的線程上運行。

Task 提供瞭一個 API 協議,用於監視、等待和訪問任務的結果值。比如,通過await關鍵字等待任務執行完成,為使用 Task 提供瞭更高層次的抽象。

使用 await 允許你在任務運行期間執行其它有用的工作,將控制權交給其調用者,直到任務完成。你不再需要依賴回調或事件來在任務完成後繼續執行後續工作。

I/O 受限異步操作

下面示例代碼演示瞭一個典型的異步 I/O 調用操作:

public Task<string> GetHtmlAsync()
{
    // 此處是同步執行
    var client = new HttpClient();
    return client.GetStringAsync("https://www.dotnetfoundation.org");
}

這個例子調用瞭一個異步方法,並返回瞭一個活動的 Task,它很可能還沒有完成。

下面第二個代碼示例增加瞭async和await關鍵字對任務進行操作:

public async Task<string> GetFirstCharactersCountAsync(string url, int count)
{
    // 此處是同步執行
    var client = new HttpClient();

    // 此處 await 掛起代碼的執行,把控制權交出去(線程可以去做別的事情)
    var page = await client.GetStringAsync("https://www.dotnetfoundation.org");

    // 任務完成後恢復瞭控制權,繼續執行後續代碼
    // 此處回到瞭同步執行

    if (count > page.Length)
    {
        return page;
    }
    else
    {
        return page.Substring(0, count);
    }
}

使用 await 關鍵字告訴當前上下文趕緊生成快照並交出控制權,異步任務執行完成後會帶著返回值去線程池排隊等待可用線程,等到可用線程後,恢復上下文,線程繼續執行後續代碼。

GetStringAsync() 方法的內部通過底層 .NET 庫調用資源(也許會調用其他異步方法),一直到 P/Invoke 互操作調用本地(Native)網絡庫。本地庫隨後可能會調用到一個系統 API(如 Linux 上 Socket 的write()API)。Task 對象將通過層層傳遞,最終返回給初始調用者。

在整個過程中,關鍵的一點是,沒有一個線程是專門用來處理任務的。雖然工作是在某種上下文中執行的(操作系統確實要把數據傳遞給設備驅動程序並中斷響應),但沒有線程專門用來等待請求的數據回返回。這使得系統可以處理更大的工作量,而不是幹等著某個 I/O 調用完成。

雖然上面的工作看似很多,但與實際 I/O 工作所需的時間相比,簡直微不足道。用一條不太精確的時間線來表示,大概是這樣的:

0-1——————–2-3

從0到1所花費的時間是await交出控制權之前所花的時間。從1到2花費的時間是GetStringAsync方法花費在 I/O 上的時間,沒有 CPU 成本。最後,從2到3花費的時間是上下文重新獲取控制權後繼續執行的時間。

CPU 受限異步操作

CPU 受限的異步代碼與 I/O 受限的異步代碼有些不同。因為工作是在 CPU 上完成的,所以沒有辦法繞開專門的線程來進行計算。使用 async 和 await 隻是為你提供瞭一種幹凈的方式來與後臺線程進行交互。請註意,這並不能為共享數據提供加鎖保護,如果你正在使用共享數據,仍然需要使用適當的同步策略。

下面是一個 CPU 受限的異步調用:

public async Task<int> CalculateResult(InputData data)
{
    // 在線程池排隊獲取線程來處理任務
    var expensiveResultTask = Task.Run(() => DoExpensiveCalculation(data));

    // 此時此處,你可以並行地處理其它工作

    var result = await expensiveResultTask;

    return result;
}

CalculateResult方法在它被調用的線程(一般可以定義為主線程)上執行。當它調用Task.Run時,會在線程池上排隊執行 CPU 受限操作 DoExpensiveCalculation,並接收一個Task<int>句柄。DoExpensiveCalculation會在下一個可用的線程上並行運行,很可能是在另一個 CPU 核上。和 I/O 受限異步調用一樣,一旦遇到await,CalculateResult的控制權就會被交給它的調用者,這樣在DoExpensiveCalculation返回結果的時候,結果就會被安排在主線程上排隊運行。

對於開發者,CUP 受限和 I/O 受限的在調用方式上沒什麼區別。區別在於所調用資源性質的不同,不必關心底層對不同資源的調用的具體邏輯。編寫代碼需要考慮的是,對於 CUP 受限的異步任務,根據實際情況考慮是否需要使其和其它任務並行執行,以加快程序的整體運行時間。

異步編程模式

最後簡單回顧一下 .NET 歷史上提供的三種執行異步操作的模式。

  • 基於任務的異步模式(Task-based Asynchronous Pattern,TAP),它使用單一的方法來表示異步操作的啟動和完成。TAP 是在 .NET Framework 4 中引入的。它是 .NET 中異步編程的推薦方法。C# 中的 async 和 await 關鍵字為 TAP 添加瞭語言支持。
  • 基於事件的異步模式(Event-based Asynchronous Pattern,EAP),這是基於事件的傳統模式,用於提供異步行為。它需要一個具有 Async 後綴的方法和一個或多個事件。EAP 是在 .NET Framework 2.0 中引入的。它不再被推薦用於新的開發。
  • 異步編程模式(Asynchronous Programming Model,APM)模式,也稱為 IAsyncResult 模式,這是使用 IAsyncResult 接口提供異步行為的傳統模式。在這種模式中,需要Begin和End方法同步操作(例如,BeginWrite和EndWrite來實現異步寫操作)。這種模式也不再推薦用於新的開發。

下面簡單舉例對三種模式進行比較。

假設有一個 Read 方法,該方法從指定的偏移量開始將指定數量的數據讀入提供的緩沖區:

public class MyClass
{
    public int Read(byte [] buffer, int offset, int count);
}

若用 TAP 異步模式來改寫,該方法將是簡單的一個 ReadAsync 方法:

public class MyClass
{
    public Task<int> ReadAsync(byte [] buffer, int offset, int count);
}

若使用 EAP 異步模式,需要額外多定義一些類型和成員:

public class MyClass
{
    public void ReadAsync(byte [] buffer, int offset, int count);
    public event ReadCompletedEventHandler ReadCompleted;
}

public delegate void ReadCompletedEventHandler(
    object sender, ReadCompletedEventArgs e);

public class ReadCompletedEventArgs : AsyncCompletedEventArgs
{
    public MyReturnType Result { get; }
}

若使用 AMP 異步模式,則需要定義兩個方法,一個用於開始執行異步操作,一個用於接收異步操作結果:

public class MyClass
{
    public IAsyncResult BeginRead(
        byte [] buffer, int offset, int count,
        AsyncCallback callback, object state);
    public int EndRead(IAsyncResult asyncResult);
}

後兩種異步模式已經過時不推薦使用瞭,這裡也不再繼續探討。歲數大點的 .NET 程序員可能比較熟悉後兩種異步模式,畢竟那時候沒有 async/await,應該沒少折騰。

以上就是c# 異步編程基礎講解的詳細內容,更多關於c# 異步編程的資料請關註WalkonNet其它相關文章!

推薦閱讀:

    None Found