.NET 程序員 CancellationToken 指南
已發表: 2022-08-23有時取消是件好事。 在我的許多 .NET 項目中,我有足夠的動力取消內部和外部流程。 微軟了解到,開發人員正在通過各種複雜的實現來處理這個常見的用例,並決定必須有更好的方法。 因此,引入了一種常見的取消通信模式CancellationToken
,它是使用較低級別的多線程和進程間通信結構構建的。 作為我對這種模式的初步研究的一部分——在挖掘了微軟實現的實際 .NET 源代碼之後——我發現CancellationToken
可以解決更廣泛的問題:訂閱應用程序的運行狀態、使用不同的操作超時觸發器,以及通過標誌進行的一般進程間通信。
預期的 CancellationToken 用例
CancellationToken
是在 .NET 4 中引入的,作為增強和標準化用於取消操作的現有解決方案的一種手段。 流行的編程語言傾向於實現四種處理取消的通用方法:
殺 | 告訴,不要接受否定的答案 | 禮貌地詢問,並接受拒絕 | 禮貌地設置標誌,讓它輪詢 | |
---|---|---|---|---|
方法 | 硬停; 稍後解決不一致的地方 | 告訴它停止,但讓它清理一切 | 一個直接但溫和的停止請求 | 要求它停止,但不要強迫它 |
概括 | 通往腐敗和痛苦的必經之路 | 允許乾淨的停止點,但必須停止 | 允許乾淨的停止點,但取消請求可能會被忽略 | 通過標誌請求取消 |
線程 | pthread_kill ,pthread_cancel (異步) | pthread_cancel (延遲模式) | 不適用 | 通過一面旗幟 |
。網 | Thread.Abort | 不適用 | Thread.Interrupt | 通過CancellationToken 中的標誌 |
爪哇 | Thread.destroy ,Thread.stop | 不適用 | Thread.interrupt | 通過標誌或Thread.interrupted |
Python | PyThreadState_SetAsyncExc | 不適用 | asyncio.Task.cancel | 通過一面旗幟 |
指導 | 不可接受; 避免這種方法 | 可接受,尤其是當一種語言不支持異常或展開時 | 如果語言支持,可以接受 | 更好,但更多的是集體努力 |
CancellationToken
位於最後一個類別,取消對話是合作的。
在 Microsoft 推出CancellationToken
後,開發社區迅速接受了它,特別是因為許多主要的 .NET API 已更新為原生使用這些令牌。 例如,從 ASP.NET Core 2.0 開始,操作支持可選的CancellationToken
參數,該參數可能會在 HTTP 請求已關閉時發出信號,從而允許取消任何操作,從而避免不必要的資源使用。
在深入了解 .NET 代碼庫之後,很明顯CancellationToken
的用途不僅限於取消。
顯微鏡下的 CancellationToken
當更仔細地查看CancellationToken
的實現時,我們看到它只是一個簡單的標誌(即ManualResetEvent
)和提供監視和更改該標誌的能力的支持基礎設施。 CancellationToken
的主要實用程序就在它的名字中,這表明這是取消操作的常用方法。 如今,任何具有異步或長時間運行操作的 .NET 庫、包或框架都允許通過這些令牌進行取消。
CancellationToken
可以通過手動將其標誌設置為“true”或在經過一定時間跨度後將其更改為“true”來觸發。 不管CancellationToken
是如何被觸發的,監控這個令牌的客戶端代碼可以通過以下三種方法之一來確定令牌標誌的值:
- 使用
WaitHandle
- 輪詢
CancellationToken
的標誌 - 當標誌的狀態通過編程訂閱更新時通知客戶端代碼
在對 .NET 代碼庫進行進一步研究之後,很明顯 .NET 團隊發現CancellationTokens
在其他與取消無關的場景中很有用。 讓我們探索其中一些高級和非品牌用例,它們使 C# 開發人員能夠通過多線程和進程間協調來簡化複雜的情況。
高級活動的 CancellationTokens
在編寫 ASP.NET Core 應用程序時,我們有時需要知道我們的應用程序何時啟動,或者我們需要將代碼注入到主機關閉過程中。 在這些情況下,我們使用IHostApplicationLifetime
接口(以前的IApplicationLifetime
)。 此接口(來自 .NET Core 的存儲庫)使用CancellationToken
來傳達三個主要事件: ApplicationStarted
、 ApplicationStopping
和ApplicationStopped
:
namespace Microsoft.Extensions.Hosting { /// <summary> /// Allows consumers to be notified of application lifetime events. /// This interface is not intended to be user-replaceable. /// </summary> public interface IHostApplicationLifetime { /// <summary> /// Triggered when the application host has fully started. /// </summary> CancellationToken ApplicationStarted { get; } /// <summary> /// Triggered when the application host is starting a graceful shutdown. /// Shutdown will block until all callbacks registered on /// this token have completed. /// </summary> CancellationToken ApplicationStopping { get; } /// <summary> /// Triggered when the application host has completed a graceful shutdown. /// The application will not exit until all callbacks registered on /// this token have completed. /// </summary> CancellationToken ApplicationStopped { get; } /// <summary> /// Requests termination of the current application. /// </summary> void StopApplication(); } }
乍一看, CancellationToken
似乎不屬於這裡,尤其是因為它們被用作事件。 然而,進一步的檢查表明這些標記非常合適:
- 它們很靈活,允許接口的客戶端以多種方式監聽這些事件。
- 它們是開箱即用的線程安全的。
- 它們可以通過組合
CancellationToken
從不同的來源創建。
儘管CancellationToken
並不適合所有事件需求,但它們非常適合僅發生一次的事件,例如應用程序啟動或停止。
CancellationToken 超時
默認情況下,ASP.NET 給我們的關閉時間很少。 在我們需要更多時間的情況下,使用內置的HostOptions
類允許我們更改此超時值。 在下面,這個超時值被包裝在一個CancellationToken
中並被輸入到底層子流程中。
IHostedService
的StopAsync
方法就是這種用法的一個很好的例子:
namespace Microsoft.Extensions.Hosting { /// <summary> /// Defines methods for objects that are managed by the host. /// </summary> public interface IHostedService { /// <summary> /// Triggered when the application host is ready to start the service. /// </summary> /// <param name="cancellationToken">Indicates that the start /// process has been aborted.</param> Task StartAsync(CancellationToken cancellationToken); /// <summary> /// Triggered when the application host is performing a graceful shutdown. /// </summary> /// <param name="cancellationToken">Indicates that the shutdown /// process should no longer be graceful.</param> Task StopAsync(CancellationToken cancellationToken); } }
從IHostedService
接口定義中可以明顯看出, StopAsync
方法採用一個CancellationToken
參數。 與該參數相關的註釋清楚地傳達了 Microsoft 對CancellationToken
的最初意圖是作為超時機製而不是取消過程。
在我看來,如果這個接口在CancellationToken
存在之前就已經存在,那麼這可能是一個TimeSpan
參數——用於指示允許處理停止操作多長時間。 根據我的經驗,超時場景幾乎總是可以轉換為具有強大附加效用的CancellationToken
。
暫時,讓我們忘記我們知道StopAsync
方法是如何設計的,而是考慮我們將如何設計該方法的協定。 首先讓我們定義需求:
-
StopAsync
方法必須嘗試停止服務。 -
StopAsync
方法應該具有正常的停止狀態。 - 無論是否達到正常停止狀態,託管服務都必須有一個最長的停止時間,由我們的超時參數定義。
通過任何形式的StopAsync
方法,我們滿足了第一個要求。 其餘的要求很棘手。 CancellationToken
通過使用標準的基於 .NET 標誌的通信工具來支持對話,從而完全滿足這些要求。
CancellationToken 作為通知機制
CancellationToken
背後最大的秘密是它只是一個標誌。 讓我們說明如何使用CancellationToken
來啟動進程而不是停止它們。
考慮以下:
- 創建一個
RandomWorker
類。 -
RandomWorker
應該有一個執行一些隨機工作的DoWorkAsync
方法。 -
DoWorkAsync
方法必須允許調用者指定何時開始工作。
public class RandomWorker { public RandomWorker(int id) { Id = id; } public int Id { get; } public async Task DoWorkAsync() { for (int i = 1; i <= 10; i++) { Console.WriteLine($"[Worker {Id}] Iteration {i}"); await Task.Delay(1000); } } }
上面的類滿足前兩個要求,剩下第三個。 我們可以使用幾個替代接口來觸發我們的工作人員,例如時間跨度或簡單的標誌:
# With a time span Task DoWorkAsync(TimeSpan startAfter); # Or a simple flag bool ShouldStart { get; set; } Task DoWorkAsync();
這兩種方法都很好,但沒有什麼比使用CancellationToken
更優雅了:
public class RandomWorker { public RandomWorker(int id) { Id = id; } public int Id { get; } public async Task DoWorkAsync(CancellationToken startToken) { startToken.WaitHandle.WaitOne(); for (int i = 1; i <= 10; i++) { Console.WriteLine($"[Worker {Id}] Iteration {i}"); await Task.Delay(1000); } } }
此示例客戶端代碼說明了此設計的強大功能:
using System; using System.Linq; using System.Threading; using System.Threading.Tasks; namespace CancelToStart { public class Program { static void Main(string[] args) { CancellationTokenSource startCts = new CancellationTokenSource(); startCts.CancelAfter(TimeSpan.FromSeconds(10)); var tasks = Enumerable.Range(0, 10) .Select(i => new RandomWorker(i)) .Select(worker => worker.DoWorkAsync(startCts.Token)) .ToArray(); Task.WaitAll(tasks, CancellationToken.None); } } }
CancellationTokenSource
將在幕後創建我們的CancellationToken
並協調所有相關進程的觸發。 在這種情況下,關聯的進程是我們的RandomWorker
,它正在等待啟動。 這種方法允許我們利用嵌入到默認CancellationToken
實現中的線程安全性。
一個擴展的 CancellationToken 工具箱
這些示例演示了CancellationToken
如何提供在其預期用例之外有用的解決方案工具箱。 這些工具可以在許多涉及基於標誌的進程間通信的場景中派上用場。 無論我們面對的是超時、通知還是一次性事件,我們都可以依靠這個經過 Microsoft 測試的優雅實現。
進一步閱讀 Toptal 工程博客:
- .NET Core:走向狂野和開源。 微軟,你怎麼花了這麼長時間?!
- 使用 ASP.NET Core 構建 ASP.NET Web API
- 如何引導和創建 .NET 項目