• LOGO OA教程 ERP教程 模切知識交流 PMS教程 CRM教程 開發文檔 其他文檔  
     
    網站管理員

    理解C#中的ValueTask

    admin
    2024年5月1日 19:32 本文熱度 151
    英文:devblogs.microsoft.com

    譯文:cnblogs.com/xiaoxiaotank/p/13206569.html

    譯者:xiaoxiaotank

    前言


    Task類是在.NET Framework 4引入的,位于System.Threading.Tasks命名空間下,它與派生的泛型類Task<TResult>已然成為.NET編程的主力,也是以async/await(C# 5引入的)語法糖為代表的異步編程模型的核心。


    隨后,我會向大家介紹.NET Core 2.0中的新成員ValueTask/ValueTask<TResult>,來幫助你在日常開發用例中降低內存分配開銷,提升異步性能。


    一、Task


    雖然Task的用法有很多,但其最核心的是“承諾(promise)”,用來表示某個操作最終完成。


    當你初始化一個操作后,會獲取一個與該操作相關的Task,當這個操作完成時,Task也同樣會完成。這個操作的完成情況可能有以下幾種:


    • 作為初始化操作的一部分同步完成,例如:訪問一些已被緩存的數據


    • 恰好在你獲取到Task實例的時候異步完成,例如:訪問雖然沒被緩存但是訪問速度非?斓臄祿


    • 你已經獲取到了Task實例,并等待了一段時間后,才異步完成,例如:訪問一些網絡數據


    由于操作可能會異步完成,所以當你想要使用最終結果時,你可以通過阻塞來等待結果返回(不過這違背了異步操作的初衷);或者,使用回調方法,它會在操作完成時被調用,.NET 4通過Task.ContinueWith方法顯式實現了這個回調方法,如:


    SomeOperationAsync().ContinueWith(task =>
    {
       try
       {
           TResult result = task.Result;
           UseResult(result);
       }
       catch(Exception ex)
       {
           HandleException(ex);
       }
    })


    而在.NET 4.5中,Task通過結合await,大大簡化了對異步操作結果的使用,它能夠優化上面說的所有情況,無論操作是同步完成、快速異步完成還是已經(隱式地)提供回調之后異步完成,都不在話下,寫法如下:


    TResult result = await SomeOperationAsync();
    UseResult(result);


    Task作為一個類(class),非常靈活,并因此帶來了很多好處。例如:


    • 它可以被任意數量的調用者并發await多次


    • 你可以把它存儲到字典中,以便任意數量的后續使用者對其進行await,進而把這個字典當成異步結果的緩存


    • 如果需要的話,你可以通過阻塞等待操作完成


    • 另外,你還可以對Task使用各種各樣的操作(稱為“組合器”,combinators),例如使用Task.WhenAny異步等待任意一個操作率先完成。


    不過,在大多數情況下其實用不到這種靈活性,只需要簡單地調用異步操作并await獲取結果就好了:


    TResult result = await SomeOperationAsync();
    UseResult(result);


    在這種用法中,我們不需要多次await task,不需要處理并發await,不需要處理同步阻塞,也不需要編寫組合器,我們只是異步等待操作的結果。


    這就是我們編寫同步代碼(例如TResult result = SomeOperation())的方式,它很自然地轉換為了async/await的方式。


    此外,Task也確實存在潛在缺陷,特別是在需要創建大量Task實例且要求高吞吐量和高性能的場景下。Task 是一個類(class),作為一個類,這意味著每創建一個操作,都需要分配一個對象,而且分配的對象越多,垃圾回收器(GC)的工作量也會越大,我們花在這個上面的資源也就越多,本來這些資源可以用于做其他事情。慶幸的是,運行時(Runtime)和核心庫在許多情況下都可以緩解這種情況。


    例如,你寫了如下方法:


    public async Task WriteAsync(byte value)
    {
       if (_bufferedCount == _buffer.Length)
       {
           await FlushAsync();
       }
       _buffer[_bufferedCount++] = value;
    }


    一般來說,緩沖區中會有可用空間,也就無需Flush,這樣操作就會同步完成。這時,不需要Task返回任何特殊信息,因為沒有返回值,返回Task與同步方法返回void沒什么區別。因此,運行時可以簡單地緩存單個非泛型Task,并將其反復用作任何同步完成的方法的結果(該單例是通過Task.CompletedTask公開的)。


    或者,你的方法是這樣的:


    public async Task<bool> MoveNextAsync()
    {
       if (_bufferedCount == 0)
       {
           // 緩存數據
           await FillBuffer();
       }
       return _bufferedCount > 0;
    }


    一般來說,我們想的是會有一些緩存數據,這樣_bufferedCount就不會等于0,直接返回true就可以了;只有當沒有緩存數據(即_bufferedCount == 0)時,才需要執行可能異步完成的操作。而且,由于只有true和false這兩種可能的結果,所以只需要兩個Task<bool>對象來分別表示true和false,因此運行時可以將這兩個對象緩存下來,避免內存分配。只有當操作異步完成時,該方法才需要分配新的Task<bool>,因為調用方在知道操作結果之前,就要得到Task<bool>對象,并且要求該對象是唯一的,這樣在操作完成后,就可以將結果存儲到該對象中。


    運行時也為其他類型型維護了一個類似的小型緩存,但是想要緩存所有內容是不切實際的。例如下面這個方法:


    public async Task<int> ReadNextByteAsync()
    {
       if (_bufferedCount == 0)
       {
          await FillBuffer();
       }
       if (_bufferedCount == 0)
       {
           return -1;
       }
       _bufferedCount--;
       return _buffer[_position++];
    }


    通常情況下,上面的案例也會同步完成。但是與上一個返回Task<bool>的案例不同,該方法返回的Int32的可能值約有40億個結果,如果將它們都緩存下來,大概會消耗數百GB的內存。雖然運行時保留了一個小型緩存,但也只保留了一小部分結果值,因此,如果該方法同步完成(緩沖區中有數據)的返回值是4,它會返回緩存的Task<int>,但是如果它同步完成的返回值是42,那就會分配一個新的Task<int>,相當于調用了Task.FromResult(42)。


    許多框架庫的實現也嘗試通過維護自己的緩存來進一步緩解這種情況。


    例如,.NET Framework 4.5中引入的MemoryStream.ReadAsync重載方法總是會同步完成,因為它只從內存中讀取數據。它返回一個Task<int>對象,其中Int32結果表示讀取的字節數。


    ReadAsync常常用在循環中,并且每次調用時請求的字節數是相同的(僅讀取到數據末尾時才有可能不同)。


    因此,重復調用通常會返回同步結果,其結果與上一次調用相同。這樣,可以維護單個Task實例的緩存,即緩存最后一次成功返回的Task實例。


    然后在后續調用中,如果新結果與其緩存的結果相匹配,它還是返回緩存的Task實例;否則,它會創建一個新的Task實例,并把它作為新的緩存Task,然后將其返回。


    即使這樣,在許多操作同步完成的情況下,仍需強制分配Task<TResult>實例并返回。


    二、同步完成時的ValueTask<TResult>


    正因如此,在.NET Core 2.0 中引入了一個新類型——ValueTask<TResult>,用來優化性能。之前的.NET版本可以通過引用NuGet包使用:


    System.Threading.Tasks.Extensions


    ValueTask<TResult>是一個結構體(struct),用來包裝TResult或Task<TResult>,因此它可以從異步方法中返回。并且,如果方法是同步成功完成的,則不需要分配任何東西:我們可以簡單地使用TResult來初始化ValueTask<TResult>并返回它。只有當方法異步完成時,才需要分配一個Task<TResult>實例,并使用ValueTask<TResult>來包裝該實例。


    另外,為了使ValueTask<TResult>更加輕量化,并為成功情形進行優化,所以拋出未處理異常的異步方法也會分配一個Task<TResult>實例,以方便ValueTask<TResult>包裝Task<TResult>,而不是增加一個附加字段來存儲異常(Exception)。


    這樣,像MemoryStream.ReadAsync這類方法將返回ValueTask<int>而不需要關注緩存,現在可以使用以下代碼:


    public override ValueTask<int> ReadAsync(byte[] buffer, int offset, int count)
    {
       try
       {
           int bytesRead = Read(buffer, offset, count);
           return new ValueTask<int>(bytesRead);
       }
       catch (Exception e)
       {
          return new ValueTask<int>(Task.FromException<int>(e));
       }
    }


    三、異步完成時的ValueTask<TResult>


    能夠編寫出在同步完成時無需為結果類型產生額外內存分配的異步方法是一項很大的突破,.NET Core 2.0引入ValueTask<TResult>的目的,就是將頻繁使用的新方法定義為返回ValueTask<TResult>而不是Task<TResult>。


    例如,我們在.NET Core 2.1中的Stream類中添加了新的ReadAsync重載方法,以傳遞Memory<byte>來替代byte[],該方法的返回類型就是ValueTask<int>。


    這樣,Streams(一般都有一種同步完成的ReadAsync方法,如前面的MemoryStream示例中所示)現在可以在使用過程中更少的分配內存。


    但是,在處理高吞吐量服務時,我們依舊需要考慮如何盡可能地避免額外內存分配,這就要想辦法減少或消除異步完成時的內存分配。


    使用await異步編程模型時,對于任何異步完成的操作,我們都需要返回代表該操作最終完成的對象:調用者需要能夠傳遞在操作完成時調用的回調方法,這就要求在堆上有一個唯一的對象,用作這種特定操作的管道,但是,這并不意味著有關操作完成后能否重用該對象的任何信息。如果對象可以重復使用,則API可以維護一個或多個此類對象的緩存,并將其復用于序列化操作,也就是說,它不能將同一對象用于多個同時進行中的異步操作,但可以復用于非并行訪問下的對象。


    在.NET Core 2.1中,為了支持這種池化和復用,ValueTask<TResult>進行了增強,不僅可以包裝TResult和Task<TResult>,還可以包裝新引入的接口IValueTaskSource<TResult>。


    類似于Task<TResult>,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);
    }


    • GetStatus用于實現諸如ValueTask<TResult>.IsCompleted之類的屬性,返回指示異步操作是否仍在掛起或是否已完成以及完成情況(成功或失。┑闹甘。


    • OnCompleted用于ValueTask<TResult>的等待者(awaiter),它與調用者提供的回調方法掛鉤,當異步操作完成時,等待者繼續執行回調方法。


    • GetResult用于檢索操作的結果,以便在操作完成后,等待者可以獲取TResult或傳播可能發生的任何異常。


    大多數開發人員永遠都不需要用到此接口(指IValueTaskSource<TResult>):方法只是簡單地將包裝該接口實例的ValueTask<TResult>實例返回給調用者,而調用者并不需要知道內部細節。該接口的主要作用是為了讓開發人員在編寫性能敏感的API時可以盡可能地避免額外內存分配。


    .NET Core 2.1中有幾個類似的API。


    最值得關注的是Socket.ReceiveAsync和Socket.SendAsync,添加了新的重載,例如:


    public ValueTask<int> ReceiveAsync(Memory<byte> buffer, SocketFlags socketFlags, CancellationToken cancellationToken = default);


    此重載返回ValueTask<int>。


    如果操作同步完成,則可以簡單地構造具有正確結果的ValueTask<int>,例如:


    int result = …;
    return new ValueTask<int>(result);


    如果它異步完成,則可以使用實現此接口的池對象:


    IValueTaskSource<int> vts = …;
    return new ValueTask<int>(vts);


    該Socket實現維護了兩個這樣的池對象,一個用于Receive,一個用于Send,這樣,每次未完成的對象只要不超過一個,即使這些重載是異步完成的,它們最終也不會額外分配內存。NetworkStream也因此受益。


    例如,在.NET Core 2.1中,Stream公開了一個方法:


    public virtual ValueTask<int> ReadAsync
    (Memory<byte> buffer, CancellationToken cancellationToken = default)
    ;


    NetworkStream的重載方法NetworkStream.ReadAsync,內部實際邏輯只是交給了Socket.ReceiveAsync去處理,所以將優勢從Socket帶到了NetworkStream中,使得NetworkStream.ReadAsync也有效地不進行額外內存分配了。


    四、非泛型的ValueTask


    當在.NET Core 2.0中引入ValueTask<TResult>時,它純粹是為了優化異步方法同步完成的情況——避免必須分配一個Task<TResult>實例用于存儲TResult。這也意味著非泛型的ValueTask是不必要的(因為沒有TResult):對于同步完成的情況,返回值為Task的方法可以返回Task.CompletedTask單例,此單例由async Task方法的運行時隱式返回。


    然而,隨著即使異步完成也要避免額外內存分配需求的出現,非泛型的ValueTask又變得必不可少。


    因此,在.NET Core 2.1中,我們還引入了非泛型的ValueTask和IValueTaskSource。它們提供泛型版本對應的非泛型版本,使用方式類似,只是GetResult返回void。


    五、實現IValueTaskSource/IValueTaskSource<TResult>


    大多數開發人員都不需要實現這兩個接口,它們也不是特別容易實現。如果您需要的話,.NET Core 2.1的內部有幾種實現可以用作參考,例如


    • AwaitableSocketAsyncEventArgs


    • AsyncOperation


    • DefaultPipeReader


    為了使想要這樣做的開發人員更輕松地進行開發,將在.NET Core 3.0中計劃引入ManualResetValueTaskSourceCore<TResult>結構體(譯注:目前已引入),用于實現接口的所有邏輯,并可以被包裝到其他實現了IValueTaskSource和IValueTaskSource<TResult>的包裝器對象中,這個包裝器對象只需要單純地將大部分實現交給該結構體就可以了。


    六、ValueTask的有效消費模式


    從表面上看,ValueTask和ValueTask<TResult>的使用限制要比Task和Task<TResult>大得多 。不過沒關系,這甚至就是我們想要的,因為主要的消費方式就是簡單地await它們。


    但是,由于ValueTask和ValueTask<TResult>可能包裝可復用的對象,因此,與Task和Task<TResult>相比,如果調用者偏離了僅await它們的設計目的,則它們在使用上實際回受到很大的限制。通常,以下操作絕對不能用在ValueTask/ValueTask<TResult>上:


    • await ValueTask/ValueTask<TResult>多次。


    因為底層對象可能已經被回收了,并已由其他操作使用。而Task/Task<TResult>永遠不會從完成狀態轉換為未完成狀態,因此您可以根據需要等待多次,并且每次都會得到相同的結果。


    • 并發await ValueTask/ValueTask<TResult>。


    底層對象期望一次只有單個調用者的單個回調來使用,并且嘗試同時等待它可能很容易引入競爭條件和細微的程序錯誤。這也是第一個錯誤操作的一個更具體的情況——await ValueTask/ValueTask<TResult>多次。相反,Task/Task<TResult>支持任意數量的并發等待


    • 使用.GetAwaiter().GetResult()時操作尚未完成。


    IValueTaskSource / IValueTaskSource<TResult>接口的實現中,在操作完成前是沒有強制要求支持阻塞的,并且很可能不會支持,所以這種操作本質上是一種競爭狀態,也不可能按照調用方的意愿去執行。相反,Task/Task<TResult>支持此功能,可以阻塞調用者,直到任務完成。


    如果您使用ValueTask/ValueTask<TResult>,并且您確實需要執行上述任一操作,則應使用.AsTask()獲取Task/Task<TResult>實例,然后對該實例進行操作。并且,在之后的代碼中您再也不應該與該ValueTask/ValueTask<TResult>進行交互。


    簡單說就是使用ValueTask/ValueTask<TResult>時,您應該直接await它(可以有選擇地加上.ConfigureAwait(false)),或直接調用AsTask()且再也不要使用它,例如:


    // 以這個方法為例
    public ValueTask<int> SomeValueTaskReturningMethodAsync();


    // GOOD
    int result = await SomeValueTaskReturningMethodAsync();

    // GOOD
    int result = await SomeValueTaskReturningMethodAsync().ConfigureAwait(false);

    // GOOD
    Task<int> t = SomeValueTaskReturningMethodAsync().AsTask();

    // WARNING
    ValueTask<int> vt = SomeValueTaskReturningMethodAsync();
    ... // 將實例存儲到本地會使它被濫用的可能性更大,
       // 不過這還好,適當使用沒啥問題

    // BAD: await 多次
    ValueTask<int> vt = SomeValueTaskReturningMethodAsync();
    int result = await vt;
    int result2 = await vt;

    // BAD: 并發 await (and, by definition then, multiple times)
    ValueTask<int> vt = SomeValueTaskReturningMethodAsync();
    Task.Run(async () => await vt);
    Task.Run(async () => await vt);

    // BAD: 在不清楚操作是否完成的情況下使用 GetAwaiter().GetResult()
    ValueTask<int> vt = SomeValueTaskReturningMethodAsync();
    int result = vt.GetAwaiter().GetResult();


    另外,開發人員可以選擇使用另一種高級模式,最好你在衡量后確定它可以帶來好處之后再使用。具體來說,ValueTask/ValueTask<TResult>確實公開了一些與操作的當前狀態有關的屬性,例如:


    • IsCompleted,如果操作尚未完成,則返回false;如果操作已完成,則返回true(這意味著該操作不再運行,并且可能已經成功完成或以其他方式完成)


    • IsCompletedSuccessfully,當且僅當它已完成并成功完成才返回true(意味著嘗試等待它或訪問其結果不會導致引發異常)


    舉個例子,對于一些執行非常頻繁的代碼,想要避免在異步執行時進行額外的性能損耗,并在某個本質上會使ValueTask/ValueTask<TResult>不再使用的操作(如await、.AsTask())時,可以先檢查這些屬性。


    例如,在 .NET Core 2.1的SocketsHttpHandler實現中,代碼在連接上發出讀操作,并返回一個ValueTask<int>實例。如果該操作同步完成,那么我們不用關注能否取消該操作。但是,如果它異步完成,在運行時就要發出取消請求,這樣取消請求會將連接斷開。由于這是一個非常常用的代碼,并且通過分析表明這樣做的確有細微差別,因此代碼的結構基本上如下:


    int bytesRead;
    {
       ValueTask<int> readTask = _connection.ReadAsync(buffer);
       if (readTask.IsCompletedSuccessfully)
       {
           bytesRead = readTask.Result;
       }
       else
       {
           using (_connection.RegisterCancellation())
           {
               bytesRead = await readTask;
           }
       }
    }


    這種模式是可以接受的,因為在ValueTask<int>的Result被訪問或自身被await之后,不會再被使用了。


    七、新異步API都應返回ValueTask/ValueTask<TResult>嗎?


    當然不是,Task/Task<TResult>仍然是默認選擇


    正如上文所強調的那樣,Task/Task<TResult>比ValueTask/ValueTask<TResult>更加容易正確使用,所以除非對性能的影響大于可用性的影響,否則Task/Task<TResult>仍然是最優的。


    此外,返回ValueTask<TResult>會比返回Task<TResult>多一些小開銷,例如,await Task<TResult>比await ValueTask<TResult>會更快一些,所以如果你可以使用緩存的Task實例(例如,你的API返回Task或Task<bool>),你或許應該為了更好地性能而仍使用Task和Task<bool>。


    而且,ValueTask/ValueTask<TResult>相比Task/Task<TResult>有更多的字段,所以當它們被await、并將它們的字段存儲在調用異步方法的狀態機中時,它們會在該狀態機對象中占用更多的空間。


    但是,如果是以下情況,那你應該使用ValueTask/ValueTask<TResult>:


    1、你希望API的調用者只能直接await它


    2、避免額外的內存分配的開銷對API很重要


    3、你預期該API常常是同步完成的,或者在異步完成時你可以有效地池化對象。


    在添加抽象、虛擬或接口方法時,您還需要考慮這些方法的重載/實現是否存在這些情況。


    八、ValueTask和ValueTask<TResult>的下一步是什么?


    對于.NET Core庫,我們將依然會看到新的API被添加進來,其返回值是Task/Task<TResult>,但在適當的地方,我們也將看到添加了新的以ValueTask/ValueTask<TResult>為返回值的API。


    ValueTask/ValueTask<TResult>的一個關鍵例子就是在.NET Core 3.0添加新的IAsyncEnumerator<T>支持。IEnumerator<T>公開了一個返回bool的MoveNext方法,異步IAsyncEnumerator<T>則會公開一個MoveNextAsync方法。


    剛開始設計此功能時,我們認為MoveNextAsync應返回Task<bool>,一般情況下,通過緩存的Task<bool>在同步完成時可以非常高效地執行此操作。


    但是,考慮到我們期望的異步枚舉的廣泛性,并且考慮到它們基于是基于接口的,其可能有許多不同的實現方式(其中一些可能會非常關注性能和內存分配),并且鑒于絕大多數的消費者將通過await foreach來使用,我們決定MoveNextAsync返回ValueTask<bool>。


    這樣既可以使同步完成案例變得很快,又可以使用可重用的對象來使異步完成案例的內存分配也減少。


    實際上,在實現異步迭代器時,C#編譯器會利用此優勢,以使異步迭代器盡可能免于額外內存分配。


    英文:https://devblogs.microsoft.com/dotnet/understanding-the-whys-whats-and-whens-of-valuetask/


    - EOF -


    該文章在 2024/5/7 11:17:34 編輯過
    關鍵字查詢
    相關文章
    正在查詢...
    點晴ERP是一款針對中小制造業的專業生產管理軟件系統,系統成熟度和易用性得到了國內大量中小企業的青睞。
    點晴PMS碼頭管理系統主要針對港口碼頭集裝箱與散貨日常運作、調度、堆場、車隊、財務費用、相關報表等業務管理,結合碼頭的業務特點,圍繞調度、堆場作業而開發的。集技術的先進性、管理的有效性于一體,是物流碼頭及其他港口類企業的高效ERP管理信息系統。
    點晴WMS倉儲管理系統提供了貨物產品管理,銷售管理,采購管理,倉儲管理,倉庫管理,保質期管理,貨位管理,庫位管理,生產管理,WMS管理系統,標簽打印,條形碼,二維碼管理,批號管理軟件。
    點晴免費OA是一款軟件和通用服務都免費,不限功能、不限時間、不限用戶的免費OA協同辦公管理系統。
    Copyright 2010-2024 ClickSun All Rights Reserved

    日本高清无卡码一区二区久久|国产高清一区二区三区|高清无码一区二区三区四区|国产精品无码一区二区免费n