深入了解異步async/await,為啥這種異步的性能這么高?異步的原理,本文徹底來個說明


使用基於 .NET 任務的異步模型可直接編寫綁定 I/O 和 CPU 的異步代碼。 該模型由 Task 和 Task<T> 類型以及 C# 和 Visual Basic 中的 async 和 await 關鍵字公開。 (有關特定語言的資源,請參見另請參閱部分。)本文解釋如何使用 .NET 異步,並深入介紹其中使用的異步框架。

划重點:該模型能編寫綁定 I/O和CUP的異步代碼,由 async+await+Task/Task<T>組成

任務和 Task<T>的工作原理簡介

任務是用於實現稱之為並發 Promise 模型的構造。 簡單地說,它們“承諾”,會在稍后完成工作,讓你使用干凈的 API 與 promise 協作。

划重點:承諾會在稍后完成工作

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

請務必將任務理解為工作的異步抽象,而非 在線程之上的抽象。 默認情況下,任務在當前線程上執行,且在適當時會將工作委托給操作系統。 可選擇性地通過 Task.Run API 顯式請求任務在獨立線程上運行。

任務會公開一個 API 協議來監視、等候和訪問任務的結果值(如 Task<T>)。 含有 await 關鍵字的語言集成可提供高級別抽象來使用任務。

任務運行時,使用 await 在任務完成前將控制讓步於其調用方,可讓應用程序和服務執行有用工作。 任務完成后代碼無需依靠回調或事件便可繼續執行。 語言和任務 API 集成會為你完成此操作。 如果正在使用 Task<T>,任務完成時,await 關鍵字還將“打開”返回的值。(面試時候說這句話就夠了~。~!) 下面進一步詳細介紹了此工作原理。

可在基於任務的異步模式 (TAP) 主題中了解有關任務以及與任務交互的不同方法的詳細信息。

深入了解針對綁定 I/O 的操作的任務

以下部分介紹了使用典型異步 I/O 調用時會出現的各種情況。 讓我們先看以下類的幾個例子。

第一個示例方法 GetHtmlAsync() 調用異步方法,並返回一個活動任務,很可能尚未完成。 第二個示例方法 GetFirstCharactersCountAsync() 還使用了 async 和 await 關鍵字對任務進行操作。

class DotNetFoundationClient
{
    // HttpClient is intended to be instantiated once per application, rather than per-use.
    private static readonly HttpClient s_client = new HttpClient();

    public Task<string> GetHtmlAsync()
    {
        // Execution is synchronous here
        var uri = new Uri("https://www.dotnetfoundation.org");

        return s_client.GetStringAsync(uri);
    }

    public async Task<string> GetFirstCharactersCountAsync(int count)
    {
        // Execution is synchronous here
        var uri = new Uri("https://www.dotnetfoundation.org");

        // Execution of GetFirstCharactersCountAsync() is yielded to the caller here
        // GetStringAsync returns a Task<string>, which is *awaited*
        var page = await s_client.GetStringAsync(uri);

        // Execution resumes when the client.GetStringAsync task completes,
        // becoming synchronous again.

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

對 GetStringAsync() 的調用通過低級別 .NET 庫進行(可能是調用其他異步方法),直到其到達 P/Invoke 互操作調用,進入本機網絡庫。 本機庫隨后可能會調入系統 API 調用(例如 Linux 上套接字的 write())。 可能會使用 TaskCompletionSource 在本機/托管邊界創建一個任務對象。 將通過層向上傳遞任務對象,對其進行操作或直接返回,最后返回到初始調用方。

在上述第二個示例方法 GetFirstCharactersCountAsync() 中,Task<T> 對象直接從 GetStringAsync 返回。 由於使用了 await 關鍵字,因此該方法會返回一個新建的任務對象。 在 GetFirstCharactersCountAsync 方法中,控制權從此位置返回給調用方。 Task<T> 對象的方法和屬性使調用者能夠監視任務的進度。GetFirstCharactersCountAsync 中剩余的代碼執行完畢時,該任務便完成。

調用系統 API 后,請求位於內核空間,一路來到操作系統的網絡子系統(例如 Linux 內核中的 /net)。 此處操作系統將對網絡請求進行異步 處理。 所用操作系統不同,細節可能有所不同(可能會將設備驅動程序調用安排為發送回運行時的信號,或者會執行設備驅動程序調用然后 有一個信號發送回來),但最終都會通知運行時網絡請求正在進行中。 此時,設備驅動程序工作處於已計划、正在進行或是已完成(請求已“通過網絡”發出),但由於這些均為異步進行,設備驅動程序可立即着手處理其他事項!

例如,在 Windows 中操作系統線程調用網絡設備驅動程序並要求它通過表示操作的中斷請求數據包 (IRP) 執行網絡操作。 設備驅動程序接收 IRP,調用網絡,將 IRP 標記為“待定”,並返回到操作系統。 由於現在操作系統線程了解到 IRP 為“待定”,因此無需再為此作業進行進一步操作,將其“返回”,這樣它就可用於完成其他工作。

請求完成且數據通過設備驅動程序返回后,會經由中斷通知 CPU 新接收到的數據。 處理中斷的方式因操作系統不同而有所不同,但最終都會通過操作系統將數據傳遞到系統互操作調用(例如,Linux 中的中斷處理程序將安排 IRQ 的下半部分通過操作系統異步向上傳遞數據)。 這也是異步發生的! 在下一個可用線程能執行異步方法且“解包”已完成任務的結果前,結果會排入隊列。

在整個過程中,關鍵點在於 沒有線程專用於運行任務。 盡管需要在一些上下文中執行工作(即,操作系統確實必須將數據傳遞到設備驅動程序並響應中斷),但沒有專用於 等待 數據從請求返回的線程。 這讓系統能處理更多的工作而不是等待某些 I/O 調用結束。

雖然這看上去需要完成許多工作,但以實際時間來計量,這遠少於執行實際 I/O 工作所花費的時間。 雖然不是完全精確,但此類調用可能的時間線如下所示:

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

  • 從點 0 到 1 所花費時間很長,直到異步方法將控制讓步於其調用方才結束。
  • 從點 1 到點 2 所用時間是花費在 I/O 上的時間,且 CPU 沒有耗時。
  • 最后,點 2 到點 3 所花費時間用於將控制(和可能的值)傳遞回異步方法,此時將再次執行。

這對服務器方案而言意味着什么?

此模型可很好地處理典型的服務器方案工作負荷。 由於沒有專用於阻止未完成任務的線程,因此服務器線程池可服務更多的 Web 請求。

考慮使用兩個服務器:一個運行異步代碼,一個不運行異步代碼。 對於本例,每個服務器只有 5 個線程可用於服務請求。 此數字太小,不切合實際,僅供演示。

假設這兩個服務器都接收 6 個並發請求。 每個請求執行一個 I/O 操作。 未運行異步代碼的服務器必須對第 6 個請求排隊,直到 5 個線程中的一個完成了 I/O 密集型工作並編寫了響應。 此時收到了第 20 個請求,由於隊列過長,服務器可能會開始變慢。

運行有異步代碼的服務器也需對第 6 個請求排隊,但由於使用了 async 和 await,I/O 密集型工作開始時,每個線程都會得到釋放,無需等到工作結束。 收到第 20 個請求時,傳入請求隊列將變得很小(如果其中還有請求的話),且服務器不會變慢。

盡管這是一個人為想象的示例,但在現實世界中其工作方式與此類似。 事實上,相比服務器將線程專用於接收到的每個請求,使用 async 和 await 能夠使服務器多處理一個數量級的請求。

這對客戶端方案而言意味着什么?

使用 async 和 await 對客戶端應用帶來的最大好處在於提高了響應能力。 盡管可以手動生成線程讓應用響應,但相比僅使用 async 和 await,生成線程的操作更加昂貴。 特別是對於手機游戲等應用而言,在涉及 I/O 時盡可能少地影響 UI 線程,這點至關重要。

更重要的是,由於綁定 I/O 的工作在 CPU 上幾乎沒有耗時,所以將整個 CPU 線程專用於執行幾乎沒有任何作用的工作將是一種資源浪費。

此外,使用 async 方法將工作調度到 UI 線程(例如更新 UI)十分簡單,且無需額外的工作(例如調用線程安全的委托)。

深入了解綁定 CPU 的操作的任務和 Task<T>

綁定 CPU 的 async 代碼與綁定 I/O 的 async 代碼有些許不同。 由於工作在 CPU 上執行,無法解決線程專用於計算的問題。 async 和 await 的運用使得可以與后台線程交互並讓異步方法調用方可響應。 請注意這不會為共享數據提供任何保護。 如果正在使用共享數據,仍需要采用合適的同步策略。

這里詳細介紹了綁定 CPU 的異步調用的方方面面:

public async Task<int> CalculateResult(InputData data)
{
    // This queues up the work on the threadpool.
    var expensiveResultTask = Task.Run(() => DoExpensiveCalculation(data));

    // Note that at this point, you can do some other work concurrently,
    // as CalculateResult() is still executing!

    // Execution of CalculateResult is yielded here!
    var result = await expensiveResultTask;

    return result;
}

CalculateResult() 在調用它的線程上執行。 調用 Task.Run 時,它會在線程池上對昂貴的綁定 CPU 的操作 DoExpensiveCalculation() 進行排隊,並收到一個 Task<int> 句柄。 DoExpensiveCalculation() 最終在下一個可用線程上並行運行(很可能在另一個 CPU 內核上)。 當 DoExpensiveCalculation() 在另一線程處理任務時,由於調用 CalculateResult() 的線程仍在執行,這時可能會出現並行工作的情況。

一旦遇到 awaitCalculateResult() 執行會讓步於調用方,在 DoExpensiveCalculation() 執行運算的同時,允許其他任務在當前線程執行。 DoExpensiveCalculation() 完成后,結果會在主線程上排隊等待運行。 最后,主線程將返回執行得到 DoExpensiveCalculation() 結果的 CalculateResult(),。

異步為什么在此處會起作用?

async 和 await 是在需要可響應性時管理綁定 CPU 的工作的最佳實踐。 存在多個可將異步用於綁定 CPU 的工作的模式。 請務必注意,使用異步成本有少許費用,不推薦緊湊循環使用它。 如何編寫此新功能的代碼完全取決於你。

 

 

本文來自微軟官方的解釋,網址是:https://docs.microsoft.com/zh-cn/dotnet/standard/async-in-depth

是最權威的講解異步的原理了。多讀幾遍,多熟悉一定要理解本文,才能真正了解異步原理。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM