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
  • 示例項目結構 在 Visual Studio 中創建一個 WinForms 應用程式後,項目結構如下所示: MyWinFormsApp/ │ ├───Properties/ │ └───Settings.settings │ ├───bin/ │ ├───Debug/ │ └───Release/ ...
  • [STAThread] 特性用於需要與 COM 組件交互的應用程式,尤其是依賴單線程模型(如 Windows Forms 應用程式)的組件。在 STA 模式下,線程擁有自己的消息迴圈,這對於處理用戶界面和某些 COM 組件是必要的。 [STAThread] static void Main(stri ...
  • 在WinForm中使用全局異常捕獲處理 在WinForm應用程式中,全局異常捕獲是確保程式穩定性的關鍵。通過在Program類的Main方法中設置全局異常處理,可以有效地捕獲並處理未預見的異常,從而避免程式崩潰。 註冊全局異常事件 [STAThread] static void Main() { / ...
  • 前言 給大家推薦一款開源的 Winform 控制項庫,可以幫助我們開發更加美觀、漂亮的 WinForm 界面。 項目介紹 SunnyUI.NET 是一個基於 .NET Framework 4.0+、.NET 6、.NET 7 和 .NET 8 的 WinForm 開源控制項庫,同時也提供了工具類庫、擴展 ...
  • 說明 該文章是屬於OverallAuth2.0系列文章,每周更新一篇該系列文章(從0到1完成系統開發)。 該系統文章,我會儘量說的非常詳細,做到不管新手、老手都能看懂。 說明:OverallAuth2.0 是一個簡單、易懂、功能強大的許可權+可視化流程管理系統。 有興趣的朋友,請關註我吧(*^▽^*) ...
  • 一、下載安裝 1.下載git 必須先下載並安裝git,再TortoiseGit下載安裝 git安裝參考教程:https://blog.csdn.net/mukes/article/details/115693833 2.TortoiseGit下載與安裝 TortoiseGit,Git客戶端,32/6 ...
  • 前言 在項目開發過程中,理解數據結構和演算法如同掌握蓋房子的秘訣。演算法不僅能幫助我們編寫高效、優質的代碼,還能解決項目中遇到的各種難題。 給大家推薦一個支持C#的開源免費、新手友好的數據結構與演算法入門教程:Hello演算法。 項目介紹 《Hello Algo》是一本開源免費、新手友好的數據結構與演算法入門 ...
  • 1.生成單個Proto.bat內容 @rem Copyright 2016, Google Inc. @rem All rights reserved. @rem @rem Redistribution and use in source and binary forms, with or with ...
  • 一:背景 1. 講故事 前段時間有位朋友找到我,說他的窗體程式在客戶這邊出現了卡死,讓我幫忙看下怎麼回事?dump也生成了,既然有dump了那就上 windbg 分析吧。 二:WinDbg 分析 1. 為什麼會卡死 窗體程式的卡死,入口門檻很低,後續往下分析就不一定了,不管怎麼說先用 !clrsta ...
  • 前言 人工智慧時代,人臉識別技術已成為安全驗證、身份識別和用戶交互的關鍵工具。 給大家推薦一款.NET 開源提供了強大的人臉識別 API,工具不僅易於集成,還具備高效處理能力。 本文將介紹一款如何利用這些API,為我們的項目添加智能識別的亮點。 項目介紹 GitHub 上擁有 1.2k 星標的 C# ...