最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

C#語言async?await工作原理示例解析

 更新時間:2023年05月31日 11:33:24   作者:微軟技術棧  
這篇文章主要為大家介紹了C#語言async?await工作原理示例解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪

正文

前不久,我們發(fā)布了《選擇 .NET 的 n 個理由》。它提供了對平臺的高層次概述,總結了各種組件和設計決策,并承諾對所涉及的領域發(fā)表更深入的文章。這是第一篇這樣深入探討 C# 和 .NET 中 async/await 的歷史、背后的設計決策和實現(xiàn)細節(jié)的文章。

對 async/await 的支持已經存在了十年之久。在這段時間里,它改變了為 .NET 編寫可擴展代碼的方式,而在不了解其底層邏輯的情況下使用該功能是可行的,也是非常常見的。在這篇文章中,我們將深入探討 await 在語言、編譯器和庫級別的工作原理,以便你可以充分利用這些有價值的功能。

不過,要做到這一點,我們需要追溯到 async/await 之前,以了解在沒有它的情況下最先進的異步代碼是什么樣子的。

最初的樣子

早在 .NET Framework 1.0中,就有異步編程模型模式,又稱 APM 模式、Begin/End 模式、IAsyncResult 模式。在高層次上,該模式很簡單。對于同步操作 DoStuff:

class Handler
{
    public int DoStuff(string arg);
}

作為模式的一部分,將有兩個相應的方法:BeginDoStuff 方法和 EndDoStuff 方法:

class Handler
{
    public int DoStuff(string arg);
    public IAsyncResult BeginDoStuff(string arg, AsyncCallback? callback, object? state);
    public int EndDoStuff(IAsyncResult asyncResult);
}

BeginDoStuff 會像 DoStuff 一樣接受所有相同的參數(shù),但除此之外,它還會接受 AsyncCallback 委托和一個不透明的狀態(tài)對象,其中一個或兩個都可以為 null。Begin 方法負責初始化異步操作,如果提供了回調(通常稱為初始操作的“延續(xù)”),它還負責確保在異步操作完成時調用回調。Begin 方法還將構造一個實現(xiàn)了 IAsyncResult 的類型實例,使用可選狀態(tài)填充 IAsyncResult 的 AsyncState 屬性:

namespace System
{
    public interface IAsyncResult
    {
        object? AsyncState { get; }
        WaitHandle AsyncWaitHandle { get; }
        bool IsCompleted { get; }
        bool CompletedSynchronously { get; }
    }
    public delegate void AsyncCallback(IAsyncResult ar);
}

然后,這個 IAsyncResult 實例將從 Begin 方法返回,并在最終調用 AsyncCallback 時傳遞給它。當準備使用操作的結果時,調用者將把 IAsyncResult 實例傳遞給 End 方法,該方法負責確保操作已完成(如果沒有完成,則通過阻塞同步等待操作完成),然后返回操作的任何結果,包括傳播可能發(fā)生的任何錯誤和異常。因此,不用像下面這樣寫代碼來同步執(zhí)行操作:

try
{
    int i = handler.DoStuff(arg);
    Use(i);
}
catch (Exception e)
{
    ... // handle exceptions from DoStuff and Use
}

可以按以下方式使用 Begin/End 方法異步執(zhí)行相同的操作:

try
{
    handler.BeginDoStuff(arg, iar =>
    {
        try
        {
            Handler handler = (Handler)iar.AsyncState!;
            int i = handler.EndDoStuff(iar);
            Use(i);
        }
        catch (Exception e2)
        {
            ... // handle exceptions from EndDoStuff and Use
        }
    }, handler);
}
catch (Exception e)
{
    ... // handle exceptions thrown from the synchronous call to BeginDoStuff
}

對于在任何語言中處理過基于回調的 API 的人來說,這應該感覺很熟悉。

然而,事情從此變得更加復雜。例如,有一個"stack dives"的問題。stack dives 是指代碼反復調用,在堆棧中越陷越深,以至于可能出現(xiàn)堆棧溢出。如果操作同步完成,Begin 方法被允許同步調用回調,這意味著對 Begin 的調用本身可能直接調用回調。同步完成的 "異步 "操作實際上是很常見的;它們不是 "異步",因為它們被保證異步完成,而只是被允許這樣做。

這是一種真實的可能性,很容易再現(xiàn)。

.NET Core 運行

在 .NET Core 上試試這個程序

using System.NET;
using System.NET.Sockets;
using Socket listener = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
listener.Bind(new IPEndPoint(IPAddress.Loopback, 0));
listener.Listen();
using Socket client = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
client.Connect(listener.LocalEndPoint!);
using Socket server = listener.Accept();
_ = server.SendAsync(new byte[100_000]);
var mres = new ManualResetEventSlim();
byte[] buffer = new byte[1];
var stream = new NetworkStream(client);
void ReadAgain()
{
    stream.BeginRead(buffer, 0, 1, iar =>
    {
        if (stream.EndRead(iar) != 0)
        {
            ReadAgain(); // uh oh!
        }
        else
        {
            mres.Set();
        }
    }, null);
};
ReadAgain();
mres.Wait();

在這里,我設置了一個相互連接的簡單客戶端套接字和服務器套接字。服務器向客戶端發(fā)送100,000字節(jié),然后客戶端繼續(xù)使用 BeginRead/EndRead 來“異步”地每次讀取一個字節(jié)。傳給 BeginRead 的回調函數(shù)通過調用 EndRead 來完成讀取,然后如果它成功讀取了所需的字節(jié),它會通過遞歸調用 ReadAgain 局部函數(shù)來發(fā)出另一個 BeginRead。然而,在 .NET Core 中,套接字操作比在 .NET Framework 上快得多,并且如果操作系統(tǒng)能夠滿足同步操作,它將同步完成(注意內核本身有一個緩沖區(qū)用于滿足套接字接收操作)。因此,這個堆棧會溢出:

因此,APM 模型中內置了補償機制。有兩種可能的方法可以彌補這一點:

1.不要允許 AsyncCallback 被同步調用。如果一直異步調用它,即使操作以同步方式完成,那么 stack dives 的風險也會消失。但是性能也是如此,因為同步完成的操作(或者快到無法觀察到它們的區(qū)別)是非常常見的,強迫每個操作排隊回調會增加可測量的開銷。

2.使用一種機制,允許調用方而不是回調方在操作同步完成時執(zhí)行延續(xù)工作。這樣,您就可以避開額外的方法框架,繼續(xù)執(zhí)行后續(xù)工作,而不深入堆棧。

APM 模式與方法2一起使用。為此,IAsyncResult 接口公開了兩個相關但不同的成員:IsCompleted 和 CompletedSynchronously。IsCompleted 告訴你操作是否已經完成,可以多次檢查它,最終它會從 false 轉換為 true,然后保持不變。相比之下,CompletedSynchronously 永遠不會改變(如果改變了,那就是一個令人討厭的 bug)。它用于 Begin 方法的調用者和 AsyncCallback 之間的通信,他們中的一個負責執(zhí)行任何延續(xù)工作。如果 CompletedSynchronously 為 false,則操作是異步完成的,響應操作完成的任何后續(xù)工作都應該留給回調;畢竟,如果工作沒有同步完成,Begin 的調用方無法真正處理它,因為還不知道操作已經完成(如果調用方只是調用 End,它將阻塞直到操作完成)。然而,如果 CompletedSynchronously 為真,如果回調要處理延續(xù)工作,那么它就有 stack dives 的風險,因為它將在堆棧上執(zhí)行比開始時更深的延續(xù)工作。因此,任何涉及到這種堆棧潛水的實現(xiàn)都需要檢查 CompletedSynchronously,并讓 Begin 方法的調用者執(zhí)行延續(xù)工作(如果它為真),這意味著回調不需要執(zhí)行延續(xù)工作。這也是 CompletedSynchronously 永遠不能更改的原因,調用方和回調方需要看到相同的值,以確保不管競爭條件如何,延續(xù)工作只執(zhí)行一次。

我們都習慣了現(xiàn)代語言中的控制流結構為我們提供的強大和簡單性,一旦引入了任何合理的復雜性,而基于回調的方法通常會與這種結構相沖突。其他主流語言也沒有更好的替代方案。

我們需要一種更好的方法,一種從 APM 模式中學習的方法,融合它正確的東西,同時避免它的陷阱。值得注意的是,APM 模式只是一種模式。運行時間、核心庫和編譯器在使用或實現(xiàn)該模式時并沒有提供任何幫助。

基于事件的異步模式

.NET Framework 2.0引入了一些 API,實現(xiàn)了處理異步操作的不同模式,這種模式主要用于在客戶端應用程序上下文中處理異步操作。這種基于事件的異步模式或 EAP 也作為一對成員出現(xiàn),這次是一個用于初始化異步操作的方法和一個用于偵聽其完成的事件。因此,我們之前的 DoStuff 示例可能被公開為一組成員,如下所示:

class Handler
{
    public int DoStuff(string arg);
    public void DoStuffAsync(string arg, object? userToken);
    public event DoStuffEventHandler? DoStuffCompleted;
}
public delegate void DoStuffEventHandler(object sender, DoStuffEventArgs e);
public class DoStuffEventArgs : AsyncCompletedEventArgs
{
    public DoStuffEventArgs(int result, Exception? error, bool canceled, object? userToken) :
        base(error, canceled, usertoken) => Result = result;
    public int Result { get; }
}

你需要用 DoStuffCompleted 事件注冊你的后續(xù)工作,然后調用 DoStuffAsync 方法;它將啟動該操作,并且在該操作完成時,調用者將異步地引發(fā) DoStuffCompleted 事件。然后,處理程序可以繼續(xù)執(zhí)行后續(xù)工作,可能會驗證所提供的 userToken 與它所期望的進行匹配,從而允許多個處理程序同時連接到事件。

這種模式使一些用例變得更簡單,同時使其他用例變得更加困難(考慮到前面的 APM CopyStreamToStream 示例,這說明了一些問題)。它沒有以廣泛的方式推出,只是在一個單獨的 .NET Framework 版本中匆匆的出現(xiàn)又消失了,盡管留下了它使用期間添加的 api,如 Ping.SendAsync/Ping.PingCompleted:

public class Ping : Component
{
    public void SendAsync(string hostNameOrAddress, object? userToken);
    public event PingCompletedEventHandler? PingCompleted;
    ...
}

然而,它確實取得了一個 APM 模式完全沒有考慮到的顯著進步,并且這一點一直延續(xù)到我們今天所接受的模型中: SynchronizationContext。

考慮到像 Windows Forms 這樣的 UI 框架。與 Windows 上的大多數(shù) UI 框架一樣,控件與特定的線程相關聯(lián),該線程運行一個消息泵,該消息泵運行能夠與這些控件交互的工作,只有該線程應該嘗試操作這些控件,而任何其他想要與控件交互的線程都應該通過發(fā)送消息由 UI 線程的泵消耗來完成操作。Windows 窗體使用 ControlBeginInvoke 等方法使這變得很容易,它將提供的委托和參數(shù)排隊,由與該控件相關聯(lián)的任何線程運行。因此,你可以這樣編寫代碼:

private void button1_Click(object sender, EventArgs e)
{
    ThreadPool.QueueUserWorkItem(_ =>
    {
        string message = ComputeMessage();
        button1.BeginInvoke(() =>
        {
            button1.Text = message;
        });
    });
}

這將卸載在 ThreadPool 線程上完成的 ComputeMessage()工作(以便在處理 UI 的過程中保持 UI 的響應性),然后在工作完成時,將委托隊列返回到與 button1 相關的線程,以更新 button1 的標簽。這很簡單,WPF 也有類似的東西,只是用它的 Dispatcher 類型:

private void button1_Click(object sender, RoutedEventArgs e){
ThreadPool.QueueUserWorkItem(_ =>
{
string message = ComputeMessage();
button1.Dispatcher.InvokeAsync(() =>
{
button1.Content = message;
});
});}

.NET MAUI 也有類似的功能。但如果我想把這個邏輯放到輔助方法中呢?

E.g.

// Call ComputeMessage and then invoke the update action to update controls.internal static void ComputeMessageAndInvokeUpdate(Action<string> update) { ... }

然后我可以這樣使用它:

private void button1_Click(object sender, EventArgs e){
    ComputeMessageAndInvokeUpdate(message => button1.Text = message);}

但是如何實現(xiàn) ComputeMessageAndInvokeUpdate,使其能夠在這些應用程序中工作呢?是否需要硬編碼才能了解每個可能的 UI 框架?這就是 SynchronizationContext 的魅力所在。我們可以這樣實現(xiàn)這個方法:

internal static void ComputeMessageAndInvokeUpdate(Action<string> update){
    SynchronizationContext? sc = SynchronizationContext.Current;
    ThreadPool.QueueUserWorkItem(_ =>
    {
        string message = ComputeMessage();
        if (sc is not null)
        {
            sc.Post(_ => update(message), null);
        }
        else
        {
            update(message);
        }
    });}

它使用 SynchronizationContext 作為一個抽象,目標是任何“調度器”,應該用于回到與 UI 交互的必要環(huán)境。然后,每個應用程序模型確保它作為 SynchronizationContext.Current 發(fā)布一個 SynchronizationContext-derived 類型,去做 "正確的事情"。例如,Windows Forms 有這個:

public sealed class WindowsFormsSynchronizationContext : SynchronizationContext, IDisposable{
    public override void Post(SendOrPostCallback d, object? state) =>
        _controlToSendTo?.BeginInvoke(d, new object?[] { state });
    ...}

WPF 有這個:

public sealed class DispatcherSynchronizationContext : SynchronizationContext{
    public override void Post(SendOrPostCallback d, Object state) =>
        _dispatcher.BeginInvoke(_priority, d, state);
    ...}

ASP.NET 曾經有一個,它實際上并不關心工作在什么線程上運行,而是關心給定的請求相關的工作被序列化,這樣多個線程就不會并發(fā)地訪問給定的 HttpContext:

internal sealed class AspNetSynchronizationContext : AspNetSynchronizationContextBase{
    public override void Post(SendOrPostCallback callback, Object state) =>
        _state.Helper.QueueAsynchronous(() => callback(state));
    ...}

這也不限于這些主要的應用程序模型。例如,xunit 是一個流行的單元測試框架,是 .NET 核心存儲庫用于單元測試的框架,它也采用了多個自定義的 SynchronizationContext。例如,你可以允許并行運行測試,但限制允許并發(fā)運行的測試數(shù)量。這是如何實現(xiàn)的呢?通過 SynchronizationContext:

public class MaxConcurrencySyncContext : SynchronizationContext, IDisposable{
    public override void Post(SendOrPostCallback d, object? state)
    {
        var context = ExecutionContext.Capture();
        workQueue.Enqueue((d, state, context));
        workReady.Set();
    }}

MaxConcurrencySyncContext 的 Post 方法只是將工作排到自己的內部工作隊列中,然后在它自己的工作線程上處理它,它根據(jù)所需的最大并發(fā)數(shù)來控制有多少工作線程。

這與基于事件的異步模式有什么聯(lián)系?EAP 和 SynchronizationContext 是同時引入的,當異步操作被啟動時,EAP 規(guī)定完成事件應該排隊到當前任何 SynchronizationContext 中。為了稍微簡化一下,System.ComponentModel 中也引入了一些輔助類型,尤其是 AsyncOperation 和 AsyncOperationManager。前者只是一個元組,封裝了用戶提供的狀態(tài)對象和捕獲的 SynchronizationContext,而后者只是作為一個簡單的工廠來捕獲并創(chuàng)建 AsyncOperation 實例。然后 EAP 實現(xiàn)將使用這些,例如 Ping.SendAsync 調用 AsyncOperationManager.CreateOperation 來捕獲 SynchronizationContext。當操作完成時,AsyncOperation 的 PostOperationCompleted 方法將被調用,以調用存儲的 SynchronizationContext 的 Post 方法。

我們需要比 APM 模式更好的東西,接下來出現(xiàn)的 EAP 引入了一些新的事務,但并沒有真正解決我們面臨的核心問題。我們仍然需要更好的東西。

輸入任務

.NET Framework 4.0 引入了 System.Threading.Tasks.Task 類型。從本質上講,Task 只是一個數(shù)據(jù)結構,表示某些異步操作的最終完成(其他框架將類似的類型稱為“promise”或“future”)。創(chuàng)建 Task 是為了表示某些操作,然后當它表示的操作邏輯上完成時,結果存儲到該 Task 中。但是 Task 提供的關鍵特性使它比 IAsyncResult 更有用,它在自己內部內置了 continuation 的概念。這一特性意味著您可以訪問任何 Task,并在其完成時請求異步通知,由任務本身處理同步,以確保繼續(xù)被調用,無論任務是否已經完成、尚未完成、還是與通知請求同時完成。為什么會有如此大的影響?如果你還記得我們對舊 APM 模式的討論,有兩個主要問題。

  • 你必須為每個操作實現(xiàn)一個自定義的 IAsyncResult 實現(xiàn):沒有內置的 IAsyncResult 實現(xiàn),任何人都可以根據(jù)需要使用。
  • 在 Begin 方法被調用之前,你必須知道當它完成時要做什么。這使得實現(xiàn)組合器和其他用于消耗和組合任意異步實現(xiàn)的通用例程成為一個重大挑戰(zhàn)。

現(xiàn)在,讓我們更好地理解它的實際含義。我們先從幾個字段開始:

class MyTask{
    private bool _completed;
    private Exception? _error;
    private Action<MyTask>? _continuation;
    private ExecutionContext? _ec;
    ...}

我們需要一個字段來知道任務是否完成(_completed),還需要一個字段來存儲導致任務失敗的任何錯誤(_error);如果我們還要實現(xiàn)一個通用的 MyTask<TResult>,那么也會有一個私有的 TResult _result 字段,用于存儲操作的成功結果。到目前為止,這看起來很像我們之前自定義的 IAsyncResult 實現(xiàn)(當然,這不是巧合)。但是現(xiàn)在最重要的部分,是 _continuation 字段。在這個簡單的實現(xiàn)中,我們只支持一個 continuation,但對于解釋目的來說這已經足夠了(真正的任務使用了一個對象字段,該字段可以是單個 continuation 對象,也可以是 continuation 對象的 List<>)。這是一個委托,將在任務完成時調用。

如前所述,與以前的模型相比,Task 的一個基本進步是能夠在操作開始后提供延續(xù)工作(回調)。我們需要一個方法來做到這一點,所以讓我們添加 ContinueWith:

public void ContinueWith(Action<MyTask> action){
    lock (this)
    {
        if (_completed)
        {
            ThreadPool.QueueUserWorkItem(_ => action(this));
        }
        else if (_continuation is not null)
        {
            throw new InvalidOperationException("Unlike Task, this implementation only supports a single continuation.");
        }
        else
        {
            _continuation = action;
            _ec = ExecutionContext.Capture();
        }
    }}

如果任務在 ContinueWith 被調用時已經被標記為完成,ContinueWith 只是排隊執(zhí)行委托。否則,該方法將存儲該委托,以便在任務完成時可以排隊繼續(xù)執(zhí)行(它還存儲了一個叫做 ExecutionContext 的東西,然后在以后調用該委托時使用它)。

然后,我們需要能夠將 MyTask 標記為完成,這意味著它所代表的異步操作已經完成。為此,我們將提供兩個方法,一個用于標記完成(" SetResult "),另一個用于標記完成并返回錯誤(" SetException "):

public void SetResult() => Complete(null);
public void SetException(Exception error) => Complete(error);
private void Complete(Exception? error){
    lock (this)
    {
        if (_completed)
        {
            throw new InvalidOperationException("Already completed");
        }
        _error = error;
        _completed = true;
        if (_continuation is not null)
        {
            ThreadPool.QueueUserWorkItem(_ =>
            {
                if (_ec is not null)
                {
                    ExecutionContext.Run(_ec, _ => _continuation(this), null);
                }
                else
                {
                    _continuation(this);
                }
            });
        }
    }}

我們存儲任何錯誤,將任務標記為已完成,然后如果之前已經注冊了 continuation,則將其排隊等待調用。

最后,我們需要一種方法來傳播任務中可能發(fā)生的任何異常(并且,如果這是一個泛型 MyTask<T>,則返回其_result);為了方便某些情況,我們還允許此方法阻塞等待任務完成,這可以通過 ContinueWith 實現(xiàn)(continuation 只是發(fā)出 ManualResetEventSlim 信號,然后調用者阻塞等待完成)。

public void Wait(){
    ManualResetEventSlim? mres = null;
    lock (this)
    {
        if (!_completed)
        {
            mres = new ManualResetEventSlim();
            ContinueWith(_ => mres.Set());
        }
    }
    mres?.Wait();
    if (_error is not null)
    {
        ExceptionDispatchInfo.Throw(_error);
    }}

基本上就是這樣?,F(xiàn)在可以肯定的是,真正的 Task 要復雜得多,有更高效的實現(xiàn),支持任意數(shù)量的 continuation,有大量關于它應該如何表現(xiàn)的按鈕(例如,continuation 應該像這里所做的那樣排隊,還是應該作為任務完成的一部分同步調用),能夠存儲多個異常而不是一個異常,具有取消的特殊知識,有大量的輔助方法用于執(zhí)行常見操作,例如 Task.Run,它創(chuàng)建一個 Task 來表示線程池上調用的委托隊列等等。

你可能還注意到,我簡單的 MyTask 直接有公共的 SetResult/SetException 方法,而 Task 沒有。實際上,Task 確實有這樣的方法,它們只是內部的,System.Threading.Tasks.TaskCompletionSource 類型作為任務及其完成的獨立“生產者”;這樣做不是出于技術上的需要,而是為了讓完成方法遠離只用于消費的東西。然后,你就可以把 Task 分發(fā)出去,而不必擔心它會在你下面完成;完成信號是創(chuàng)建任務的實現(xiàn)細節(jié),并且通過保留 TaskCompletionSource 本身來保留完成它的權利。(CancellationToken 和 CancellationTokenSource 遵循類似的模式:CancellationToken 只是 CancellationTokenSource 的一個結構封裝器,只提供與消費取消信號相關的公共區(qū)域,但沒有產生取消信號的能力,而產生取消信號的能力僅限于能夠訪問 CancellationTokenSource的人。)

當然,我們可以為這個 MyTask 實現(xiàn)組合器和輔助器,就像 Task 提供的那樣。

想要一個簡單的 MyTask.WhenAll?

public static MyTask WhenAll(MyTask t1, MyTask t2){
    var t = new MyTask();
    int remaining = 2;
    Exception? e = null;
    Action<MyTask> continuation = completed =>
    {
        e ??= completed._error; // just store a single exception for simplicity
        if (Interlocked.Decrement(ref remaining) == 0)
        {
            if (e is not null) t.SetException(e);
            else t.SetResult();
        }
    };
    t1.ContinueWith(continuation);
    t2.ContinueWith(continuation);
    return t;}

想要一個 MyTask.Run?你得到了它:

public static MyTask Run(Action action){
    var t = new MyTask();
    ThreadPool.QueueUserWorkItem(_ =>
    {
        try
        {
            action();
            t.SetResult();
        }
        catch (Exception e)
        {
            t.SetException(e);
        }
    });
    return t;}

一個 MyTask.Delay 怎么樣?當然可以:

public static MyTask Delay(TimeSpan delay){
    var t = new MyTask();
    var timer = new Timer(_ => t.SetResult());
    timer.Change(delay, Timeout.InfiniteTimeSpan);
    return t;}

有了 Task,.NET 中之前的所有異步模式都將成為過去。在以前使用 APM 模式或 EAP 模式實現(xiàn)異步實現(xiàn)的地方,都會公開新的 Task 返回方法。

▌ValueTasks

時至今日,Task 仍然是 .NET 中異步處理的主力,每次發(fā)布都有新方法公開,并且在整個生態(tài)系統(tǒng)中都例行地返回 Task 和  Task<TResult>。然而,Task 是一個類,這意味著創(chuàng)建一個類需要分配內存。在大多數(shù)情況下,為一個長期異步操作額外分配內存是微不足道的,除了對性能最敏感的操作之外,不會對所有操作的性能產生有意義的影響。不過,如前所述,異步操作的同步完成是相當常見的。引入 Stream.ReadAsync 是為了返回一個 Task<int>,但如果你從一個 BufferedStream 中讀取數(shù)據(jù),很有可能很多讀取都是同步完成的,因為只需要從內存中的緩沖區(qū)中讀取數(shù)據(jù),而不是執(zhí)行系統(tǒng)調用和真正的 I/O 操作。不得不分配一個額外的對象來返回這樣的數(shù)據(jù)是不幸的(注意,APM 也是這樣的情況)。對于返回 Task 的非泛型方法,該方法可以只返回一個已經完成的單例任務,而實際上 Task.CompletedTask 提供了一個這樣的單例 Task。但對于 Task<TResult> 來說,不可能為每個可能的結果緩存一個 Task。我們可以做些什么來讓這種同步完成更快呢?

緩存一些 Task<TResult> 是可能的。例如,Task<bool> 非常常見,而且只有兩個有意義的東西需要緩存:當結果為 true 時,一個 Task<bool>,當結果為 false 時,一個 Task<bool>。或者,雖然我們不想緩存40億個 Task<int> 來容納所有可能的 Int32 結果,但小的 Int32 值是非常常見的,因此我們可以緩存一些值,比如-1到8。或者對于任意類型,default 是一個合理的通用值,因此我們可以緩存每個相關類型的 Task<TResult>,其中 Result 為 default(TResult)。事實上,Task.FromResult 今天也是這樣做的 (從最近的 .NET 版本開始),使用一個小型的可復用的 Task<TResult> 單例緩存,并在適當時返回其中一個,或者為準確提供的結果值分配一個新的 Task<TResult>。可以創(chuàng)建其他方案來處理其他合理的常見情況。例如,當使用 Stream.ReadAsync 時,在同一個流上多次調用它是合理的,而且每次調用時允許讀取的字節(jié)數(shù)都是相同的。實現(xiàn)能夠完全滿足 count 請求是合理的。這意味著 Stream.ReadAsync 重復返回相同的 int 值是很常見的。為了避免這種情況下的多次分配,多個 Stream 類型(如 MemoryStream)會緩存它們最后成功返回的 Task<int>,如果下一次讀取也同步完成并成功獲得相同的結果,它可以只是再次返回相同的 Task<int>,而不是創(chuàng)建一個新的。但其他情況呢?在性能開銷非常重要的情況下,如何更普遍地避免對同步完成的這種分配?

這就是 ValueTask<TResult> 的作用。ValueTask<TResult> 最初是作為 TResult 和 Task<TResult> 之間的一個區(qū)分并集。說到底,拋開那些花哨的東西,這就是它的全部 (或者,更確切地說,曾經是),是一個即時的結果,或者是對未來某個時刻的一個結果的承諾:

public readonly struct ValueTask<TResult>{
   private readonly Task<TResult>? _task;
   private readonly TResult _result;
   ...}

然后,一個方法可以返回這樣一個 ValueTask<TResult>,而不是一個 Task<TResult>,如果 TResult 在需要返回的時候已經知道了,那么就可以避免 Task<TResult> 的分配,代價是一個更大的返回類型和稍微多一點間接性。

然而,在一些超級極端的高性能場景中,即使在異步完成的情況下,您也希望能夠避免 Task<TResult> 分配。例如,Socket 位于網絡堆棧的底部,Socket 上的 SendAsync 和 ReceiveAsync 對于許多服務來說是非常熱門的路徑,同步和異步完成都非常常見(大多數(shù)同步發(fā)送完成,許多同步接收完成,因為數(shù)據(jù)已經在內核中緩沖了)。如果在一個給定的 Socket 上,我們可以使這樣的發(fā)送和接收不受分配限制,而不管操作是同步完成還是異步完成,這不是很好嗎?

這就是 System.Threading.Tasks.Sources.IValueTaskSource<TResult> 進入的地方:

public interface IValueTaskSource<out TResult>{
    ValueTaskSourceStatus GetStatus(short token);
    void OnCompleted(Action<object?> continuation, object? state, short token, ValueTaskSourceOnCompletedFlags flags);
    TResult GetResult(short token);}

IValueTaskSource<TResult> 接口允許一個實現(xiàn)為 ValueTask<TResult> 提供自己的支持對象,使該對象能夠實現(xiàn)像 GetResult 這樣的方法來檢索操作的結果,以及 OnCompleted 來連接操作的延續(xù)。就這樣,ValueTask<TResult> 對其定義進行了一個小小的更改,其 Task<TResult>? _task 字段替換為 object? _obj 字段:

public readonly struct ValueTask<TResult>{
   private readonly object? _obj;
   private readonly TResult _result;
   ...}

以前 _task 字段要么是 Task<TResult> 要么是 null,現(xiàn)在 _obj 字段也可以是 IValueTaskSource<TResult>。一旦 Task<TResult> 被標記為已完成,它將保持完成狀態(tài),并且永遠不會轉換回未完成的狀態(tài)。相比之下,實現(xiàn) IValueTaskSource<TResult> 的對象對實現(xiàn)有完全的控制權,可以自由地在完成狀態(tài)和不完成狀態(tài)之間雙向轉換,因為 ValueTask<TResult> 的契約是一個給定的實例只能被消耗一次,因此從結構上看,它不應該觀察到底層實例的消耗后變化(這就是 CA2012等分析規(guī)則存在的原因)。這就使得像 Socket 這樣的類型能夠將 IValueTaskSource<TResult> 的實例集中起來,用于重復調用。Socket 最多可以緩存兩個這樣的實例,一個用于讀,一個用于寫,因為99.999%的情況是在同一時間最多只有一個接收和一個發(fā)送。

我提到了 ValueTask<TResult>,但沒有提到 ValueTask。當只處理避免同步完成的分配時,使用非泛型 ValueTask(代表無結果的無效操作)在性能上沒有什么好處,因為同樣的條件可以用 Task.CompletedTask 來表示。但是,一旦我們關心在異步完成的情況下使用可池化的底層對象來避免分配的能力,那么這對非泛型也很重要。因此,當 IValueTaskSource<TResult> 被引入時,IValueTaskSource 和 ValueTask 也被引入。

因此,我們有 Task、Task<TResult>、ValueTask 和 ValueTask<TResult>。我們能夠以各種方式與它們交互,表示任意的異步操作,并連接 continuation 來處理這些異步操作的完成。

下期文章,我們將繼續(xù)介紹 C# 迭代器,歡迎持續(xù)關注。

以上就是C#語言async await工作原理示例解析的詳細內容,更多關于C# async await工作原理的資料請關注腳本之家其它相關文章!

相關文章

最新評論

任丘市| 乐都县| 天台县| 含山县| 肇东市| 垣曲县| 高安市| 承德县| 江永县| 木里| 沂水县| 收藏| 绥芬河市| 赞皇县| 合江县| 芒康县| 扎赉特旗| 德格县| 美姑县| 萍乡市| 忻城县| 齐河县| 高碑店市| 南昌县| 民和| 青川县| 开鲁县| 巴青县| 金湖县| 民乐县| 凤山县| 濉溪县| 都匀市| 天门市| 高陵县| 十堰市| 江达县| 法库县| 柘城县| 广州市| 皮山县|