ConfigureAwait in .NET8

来源:https://www.cnblogs.com/ms27946/archive/2023/11/24/ConfigureAwait_in_NET8_CN.html
-Advertisement-
Play Games

ConfigureAwait in .NET8 ConfigureAwait(true) 和 ConfigureAwait(false) 首先,讓我們回顧一下原版 ConfigureAwait 的語義和歷史,它採用了一個名為 continueOnCapturedContext 的布爾參數。 當對任務 ...


ConfigureAwait in .NET8

ConfigureAwait(true) 和 ConfigureAwait(false)

首先,讓我們回顧一下原版 ConfigureAwait 的語義和歷史,它採用了一個名為 continueOnCapturedContext 的布爾參數。

當對任務(TaskTask<T>ValueTaskValueTask<T>)執行 await 操作時,其預設行為是捕獲“上下文”的;稍後,當任務完成時,該 async 方法將在該上下文中繼續執行。“上下文”是 SynchronizationContext.CurrentTaskScheduler.Current(如果未提供上下文,則回退到線程池上下文)。通過使用 ConfigureAwait(continueOnCapturedContext: true) 可以明確這種在捕獲上下文中繼續的預設行為。

如果不想在該上下文上恢復,ConfigureAwait(continueOnCapturedContext: false) 就很有用。使用 ConfigureAwait(false) 時,非同步方法會在任何可用的線程池線程上恢復。

ConfigureAwait(false) 的歷史很有趣(至少對我來說是這樣)。最初,社區建議在所有可能的地方使用 ConfigureAwait(false),除非需要上下文。這也是我在 Async 最佳實踐一文中推薦的立場。在那段時間里,我們就預設為 true 的原因進行了多次討論,尤其是那些不得不經常使用 ConfigureAwait(false) 的庫開發人員。

不過,多年來,”儘可能使用 ConfigureAwait(false)“的建議已被修改。第一次(儘管是微小的)變化是,不再是”儘可能使用 ConfigureAwait(false)“,而是出現了更簡單的指導原則:在庫代碼中使用 ConfigureAwait(false),而不要在應用代碼中使用。這條準則更容易理解和遵循。儘管如此,關於必須使用 ConfigureAwait(false) 的抱怨仍在繼續,並不時有人要求在整個項目範圍內更改預設值。出於語言一致性的考慮,C# 團隊總是拒絕這些請求。

最近(具體來說,自從 ASP.NET 在 ASP.NET Core 中放棄了 SynchronizationContext 並修複了所有需要 sync-over-async(即同步套非同步代碼) 的地方之後),C# 團隊開始放棄使用 ConfigureAwait(false)。作為一名庫作者,我完全理解讓 ConfigureAwait(false) 在代碼庫中隨處可見是多麼令人討厭!有些庫作者決定不再使用 ConfigureAwait(false)。就我自己而言,我仍然在我的庫中使用 ConfigureAwait(false),但我理解這種挫敗感。

既然談到了 ConfigureAwait(false),我想指出幾個常見的誤解:

  1. ConfigureAwait(false) 並不是避免死鎖的好方法。這不是它的目的,充其量只是一個值得商榷的解決方案。為了在直接阻塞時避免死鎖,你必須確保所有非同步代碼都使用 ConfigureAwait(false),包括庫和運行時中的代碼。這並不是一個非常容易維護的解決方案。還有更好的解決方案
  2. ConfigureAwait 配置的是 await,而不是任務。例如,SomethingAsync().ConfigureAwait(false).GetAwaiter().GetResult() 中的 ConfigureAwait(false) 完全沒有任何作用。同樣,var task = SomethingAsync(); task.ConfigureAwait(false); await task; 中的 await 仍在捕獲的上下文中繼續,完全忽略了 ConfigureAwait(false)。多年來,我見過這兩種錯誤。
  3. ConfigureAwait(false) 並不意味著”線上程池線程上運行此方法的後續部分“或”在不同的線程上運行此方法的後續部分“。它只在 await 暫停執行並稍後恢復非同步方法時生效。具體來說,如果 await 的任務已經完成,它將不會暫停執行;在這種情況下,ConfigureAwait 將不會起作用,因為await 會同步繼續執行。

好了,既然我們已經重新理解了 ConfigureAwait(false),下麵就讓我們看看 ConfigureAwait 在 .NET8 中是如何得到增強的。ConfigureAwait(true)ConfigureAwait(false) 仍具有相同的行為。但是,有一種新的 ConfigureAwait 即將出現!

ConfigureAwait(ConfigureAwaitOptions)

ConfigureAwait 有幾個新選項。ConfigureAwaitOptions 是一種新類型,它提供了配置 awaitables 的所有不同方法:

namespace System.Threading.Tasks;
[Flags]
public enum ConfigureAwaitOptions
{
    None = 0x0,
    ContinueOnCapturedContext = 0x1,
    SuppressThrowing = 0x2,
    ForceYielding = 0x4,
}

首先,請註意:這是一個 Flags 枚舉;這些選項的任何組合都可以一起使用。

接下來我要指出的是,至少在 .NET8 中,ConfigureAwait(ConfigureAwaitOptions) 僅適用於 TaskTask<T>。它還沒有添加到 ValueTask/ValueTask<T>。未來的 .NET 版本有可能為 ValueTask 添加 ConfigureAwait(ConfigureAwaitOptions),但目前它僅適用於引用任務,因此如果您想在 ValueTask 中使用這些新選項,則需要調用 AsTask

現在,讓我們依次講解這些選項。

ConfigureAwaitOptions.None 和 ConfigureAwaitOptions.ContinueOnCapturedContext

這兩個選項都很熟悉,但有一點不同。

ConfigureAwaitOptions.ContinueOnCapturedContext--從名字就能猜到與 ConfigureAwait(continueOnCapturedContext: true) 相同。換句話說,await 將捕獲上下文,併在該上下文上繼續執行非同步方法。

Task task = ...;

// 下麵做的事情相同
await task;
await task.ConfigureAwait(continueOnCapturedContext: true);
await task.ConfigureAwait(ConfigureAwaitOptions.ContinueOnCapturedContext);

ConfigureAwaitOptions.NoneConfigureAwait(continueOnCapturedContext: false) 相同。換句話說,除了不捕獲上下文外,await 的行為完全正常;假設 await 確實產生了結果(即任務尚未完成),那麼非同步方法將在任何可用的線程池線程上繼續執行。

Task task = ...;

// 下麵兩行代碼效果一樣
await task.ConfigureAwait(continueOnCapturedContext: false);
await task.ConfigureAwait(ConfigureAwaitOptions.None);

這裡有一個轉折點:使用新選項後,預設情況下不會捕獲上下文!除非你在標記中明確包含 ContinueOnCapturedContext,否則上下文將不會被捕獲。當然,await 本身的預設行為不會改變:在沒有任何 ConfigureAwait 的情況下,await 的行為將與使用了 ConfigureAwait(true)ConfigureAwaitOptions.ContinueOnCapturedContext) 時一樣。

Task task = ...;

// 預設的行為還是會繼續捕捉上下文
await task;

// 預設選項 (ConfigureAwaitOptions.None): 不會捕捉上下文
await task.ConfigureAwait(ConfigureAwaitOptions.None);

因此,在開始使用這個新的 ConfigureAwaitOptions 枚舉時,請記住這一點。

ConfigureAwaitOptions.SuppressThrowing

SuppressThrowing 標誌可抑制等待任務時可能出現的異常。在正常情況下,await 會通過在 await 時重新引發異常來觀察任務異常。通常情況下,這正是你想要的行為,但在某些情況下,你只想等待任務完成,而不在乎任務是成功完成還是出現異常。那麼 SuppressThrowing 選項允許您等待任務完成,而不觀察其結果。

Task task = ...;

// 下麵兩行代碼等價
await task.ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing);
try { await task.ConfigureAwait(false); } catch { }

我預計這將與取消任務一起發揮最大作用。在某些情況下,有些代碼需要先取消任務,然後等待現有任務完成後再啟動替代任務。在這種情況下,SuppressThrowing 將非常有用:代碼可以使用 SuppressThrowing 等待,當任務完成時,無論任務是成功、取消還是出現異常,方法都將繼續。

// 取消舊任務並等待完成,忽略異常情況
_cts.Cancel();
await _task.ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing);

// 開啟新任務
_cts = new CancellationTokenSource();
_task = SomethingAsync(_cts.Token);

如果使用 SuppressThrowing 標誌等待,異常就會被視為”已觀察到“,因此不會引發 TaskScheduler.UnobservedTaskException 異常。我們的假設是,你在等待任務時故意丟棄了異常,所以它不會被認為是未觀察到的。

TaskScheduler.UnobservedTaskException += (_, __) => { Console.WriteLine("never printed"); };

Task task = Task.FromException(new InvalidOperationException());
await task.ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing);
task = null;

GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();

Console.ReadKey();

這個標記還有另一個考慮因素。當與 Task 一起使用時,其語義很清楚:如果任務失敗了,異常將被忽略。但是,同樣的語義對 Task<T> 並不完全適用,因為在這種情況下,await 表達式需要返回一個值(T 類型)。目前還不清楚在忽略異常的情況下返回 T 的哪個值合適,因此當前的行為是在運行時拋出 ArgumentOutOfRangeException。為了幫助在編譯時捕捉到這種情況,最近添加了一個新的警告:CA2261 ConfigureAwaitOptions.SuppressThrowing 僅支持非泛型任務。該規則預設為警告,但我建議將其設為錯誤,因為它在運行時總是會失敗。

Task<int> task = Task.FromResult(13);

// 在構建時導致 CA2261 警告,在運行時導致 ArgumentOutOfRangeException。
await task.ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing);

最後要說明的是,除了 await 之外,該標記還影響同步阻塞。具體來說,您可以調用 .GetAwaiter().GetResult() 來阻塞從 ConfigureAwait 返回的 awaiter。無論使用 await 還是 GetAwaiter().GetResult()SuppressThrowing 標記都會導致異常被忽略。以前,當 ConfigureAwait 只接受一個布爾參數時,你可以說”ConfigureAwait 配置了 await“;但現在你必須說得更具體:”ConfigureAwait 返回了一個已配置的 await“。現在,除了 await 的行為外,配置的 awaitable 還有可能修改阻塞代碼的行為。除了修改 await 的行為之外。現在的 ConfigureAwait 可能有點誤導性,但它仍然主要用於配置 await。當然,不推薦在非同步代碼中進行阻塞操作。

Task task = Task.Run(() => throw new InvalidOperationException());

// 同步阻塞任務(不推薦)。不會拋出異常。
task.ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing).GetAwaiter().GetResult();

ConfigureAwaitOptions.ForceYielding

最後一個標誌是 ForceYielding 標誌。我估計這個標誌很少會用到,但當你需要它時,你就需要它!

ForceYielding 類似於 Task.YieldYield 返回一個特殊的 awaitable,它總是聲稱尚未完成,但會立即安排其繼續。這意味著 await 始終以非同步方式執行,讓出給調用者,然後非同步方法儘快繼續執行。await 的正常行為是檢查可等待對象是否完成,如果完成,則繼續同步執行;ForceYielding 阻止了這種同步行為,強制 await 以非同步方式執行。

就我個人而言,我發現強制非同步行為在單元測試中最有用。在某些情況下,它還可以用來避免堆棧潛入。在實現非同步協調基元(如我的 AsyncEx 庫中的原語)時,它也可能很有用。基本上,在任何需要強制 await 以非同步方式運行的地方,都可以使用 ForceYielding 來實現。

我覺得有趣的一點是,使用 ForceYieldingawait 會讓 await 的行為與 JavaScript 中的一樣。在 JavaScript 中,await 總是會產生結果,即使你傳遞給它一個已解析的 Promise 也是如此。在 C# 中,您現在可以使用 ForceYielding 來等待一個已完成的任務,await 的行為就好像它尚未完成一樣,就像 JavaScript 的 await 一樣。

static async Task Main()
{
  Console.WriteLine(Environment.CurrentManagedThreadId); // main thread
  await Task.CompletedTask;
  Console.WriteLine(Environment.CurrentManagedThreadId); // main thread
  await Task.CompletedTask.ConfigureAwait(ConfigureAwaitOptions.ForceYielding);
  Console.WriteLine(Environment.CurrentManagedThreadId); // thread pool thread
}

請註意,ForceYielding 本身也意味著不在捕獲的上下文中繼續執行,因此等同於說”將該方法的剩餘部分調度到線程池“或者”切換到線程池線程“。

// ForceYielding 強制 await 以非同步方式執行。
// 缺少 ContinueOnCapturedContext 意味著該方法將線上程池線程上繼續執行。
// 因此,該語句之後的代碼將始終線上程池線程上運行。
await task.ConfigureAwait(ConfigureAwaitOptions.ForceYielding);

Task.Yield 將在捕獲的上下文中恢復執行,因此它與僅使用 ForceYielding 不完全相同。實際上,它類似於帶有 ContinueOnCapturedContextForceYielding

// 下麵兩行代碼效果相同
await Task.Yield();
await Task.CompletedTask.ConfigureAwait(ConfigureAwaitOptions.ForceYielding | ConfigureAwaitOptions.ContinueOnCapturedContext);

當然,ForceYielding 的真正價值在於它可以應用於任何任務。以前,在需要進行讓步的情況下,您必須要麼添加單獨的 await Task.Yield() 語句,要麼創建自定義的可等待對象。現在有了可以應用於任何任務的 ForceYielding,這些操作就不再必要了。

拓展閱讀

很高興看到 .NET 團隊在多年後仍然在改進 async/await 的功能!

如果您對 ConfigureAwaitOptions 背後的歷史和設計討論更感興趣,可以查看相關的 Pull Request。在發佈之前,曾經有一個名為ForceAsynchronousContinuation 的選項,但後來被刪除了。它具有更加複雜的用例,基本上可以覆蓋 await 的預設行為,將非同步方法的繼續操作調度為 ExecuteSynchronously。也許未來的更新會重新添加這個選項,或者也許將來的更新會為 ValueTask 添加 ConfigureAwaitOptions 的支持。我們只能拭目以待!

原文鏈接

ConfigureAwait in .NET 8 (stephencleary.com)

知識共用許可協議
本作品採用知識共用署名-非商業性使用-相同方式共用 4.0 國際許可協議進行許可。
您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • IJPay 的宗旨是讓支付觸手可及。封裝了微信支付、QQ 支付、支付寶支付、京東支付、銀聯支付、PayPal 支付等常用的支付方式以及各種常用的介面。 ...
  • 就在今天凌晨,Spring Boot 3.2正式發佈了!該版本是在Java 21正式發佈之後的重要支持版本,所以在該版本中包含大量對Java 21支持的優化。 下麵,我們分別通過Spring官方發佈的博文和Josh Long長達80+分鐘的介紹視頻,一起認識一下Spring Boot 3.2最新版本 ...
  • 作者:敲敲敲敲暴你腦袋 鏈接:https://juejin.cn/post/7290758270577557539 一入國企深似海,捆綁越多,你走得越難,有的人甚至終身被困於此,任人搓揉捏扁。離職就要走得乾脆點,不要回頭,通常更煩的事情可能還在後頭,早走早解脫! 1.國企怎樣的? 有人說進國企,基本 ...
  • 在深度學習中,PyTorch和NumPy是兩個常用的工具,用於處理和轉換數據。PyTorch是一個基於Python的科學計算庫,用於構建神經網路和深度學習模型。NumPy是一個用於科學計算的Python庫,提供了一個強大的多維數組對象和用於處理這些數組的函數。 在深度學習中,通常需要將數據從NumP ...
  • 作為一名有著Java背景的開發者,你無疑已經習慣了Java那嚴格的類型系統和細緻的訪問控制機制。轉向Python,你會發現一個截然不同的編程世界。Python的面向對象編程(OOP)方式為代碼組織提供了更高的自由度和靈活性,這種變化可能會給你帶來新鮮感,同時也是一個挑戰。需要註意的是,Python的... ...
  • C++ 動態實例化(new 和 malloc) malloc / free 工作原理 malloc 是 stdlib.h 庫中的函數,聲明為 void *__cdecl malloc(size_t _Size); 原理: malloc 函數沿空閑鏈表(位於記憶體 堆空間 中)申請一塊滿足需求的記憶體塊, ...
  • 一:背景 1. 講故事 最近接連遇到了幾起 2G 虛擬地址緊張 導致的程式崩潰,基本上 90% 都集中在醫療行業,真的很無語,他們用的都是一些上古的 XP,Windows7 x86,我也知道技術人很難也基本無法推動硬體系統和設備的升級,這裡蘊含了巨大的人情世故。 寫這一篇的目的是想系統化的整理一下如 ...
  • 介紹 C#跨平臺UI框架 提供NETStandard2.0和net4的庫,通過Netcore可以跨平臺,支持Windows、Mac、Linux,Net4的可以支持XP。 各個平臺運行效果一致,不依賴系統控制項。 支持窗體,控制項任意透明,支持異形窗體,支持SVG圖標顯示。 支持動畫,數據綁定,Mvvm模 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...