第十一章:C#異步函數與面向對象編程的融合
第十一章:C#異步函數與面向對象編程的融合
在現代軟件開發中,異步編程已經成為提升應用程序性能和用戶體驗的關鍵技術。然而,C# 的異步特性是函數式編程范式的產物,與傳統面向對象編程存在本質上的差異。當需要將異步操作融入面向對象的設計中時,你可能會遇到許多獨特的挑戰,例如接口方法的異步化、構造函數中的異步邏輯處理、異步屬性與事件的設計等。
11.1 異步接口及繼承
問題
在接口或基類中定義一個方法時,如何將其異步化?
解決方案
核心理念是:async 是一個實現細節,不能直接用于接口或抽象方法的定義,但可以通過返回 Task 或 Task<T> 來定義異步的簽名。
- 接口方法或抽象方法:直接返回
Task或Task<T>,表示其實現可能是異步的。 - 實現方法:在實現時使用
async關鍵字完成具體的異步操作。
這樣,異步接口的設計兼容同步實現和異步實現,也符合靈活性和規范性的需求。
代碼示例
- 定義接口
接口通過返回 Task 或 Task<T> 的方式定義“異步方法”。這樣的方法本質上是“可等待的”,可以通過 await 使用:
interface IMyAsyncInterface
{
Task<int> CountBytesAsync(HttpClient client, string url);
}
- 異步實現
類實現接口,并在方法體內使用async/await執行異步操作:
class MyAsyncClass : IMyAsyncInterface
{
public async Task<int> CountBytesAsync(HttpClient client, string url)
{
var bytes = await client.GetByteArrayAsync(url);// 異步操作
return bytes.Length;
}
}
- 消費異步接口
在使用該接口時,可以直接通過 await 調用其方法:
async Task UseMyInterfaceAsync(HttpClient client, IMyAsyncInterface service)
{
var result = await service.CountBytesAsync(client, "http://example.com");
Console.WriteLine(result);
}
- 同步實現
為了方便測試或兼容需求,接口的實現方法可以同步返回結果,而不需要實際執行異步操作:
class MyAsyncClassStub : IMyAsyncInterface
{
public Task<int> CountBytesAsync(HttpClient client, string url)
{
// 返回一個固定的結果,而無需異步計算
return Task.FromResult(42);
}
}
注意:如下這種也是普通同步方法,因為方法并沒有異步調用任何方法,只是創建并啟動了一個Task,然后返回這個Task而已,所以是同步方法(編譯器不會為
DoSomethingAsync生成狀態機):public Task<int> DoSomethingAsync() { return Task.Run(async () => { await Task.Delay(1000); return 5; }); }但是如果是這樣寫,那就是異步方法了(編譯器會為
DoSomethingAsync生成狀態機):public async Task<int> DoSomethingAsync() { return await Task.Run(async () => { await Task.Delay(1000); return 5; }); }
小結
-
接口方法不能包含
async關鍵字:async是方法實現的一部分,用于告訴編譯器生成狀態機來處理異步操作,而接口和抽象類僅定義方法的簽名,不涉及實現細節。因此,接口和抽象類的方法不能使用async。
-
異步接口的本質:
- 異步接口的核心在于返回類型是
Task或Task<T>,而不是方法本身是否使用async。 - 調用方只需知道方法返回可等待的任務,而無需關心具體實現是異步的還是同步的。
- 異步接口的核心在于返回類型是
-
異步任務的本質:
- 一個
Task或Task<T>代表一項任務,可能是未完成的任務(將在未來完成)或已完成的任務(立即返回結果)。 - 通過這種機制,調用方可以統一使用
await等待任務的完成,而不需要感知任務的具體狀態。
- 一個
-
實現的靈活性:
- 接口方法可以通過真正的異步操作(如
async和await)實現,也可以通過同步方式(如Task.FromResult)實現。 - 這種靈活性允許在不同場景下選擇合適的實現策略:
- 如果存在實際的異步操作(如 I/O、網絡請求),應采用真正的異步實現。
- 如果不需要異步操作,可以返回一個已完成的任務(例如
Task.FromResult)。
- 接口方法可以通過真正的異步操作(如
-
異步化策略:
- 優先異步:當方法涉及 I/O 操作(如文件讀寫、網絡請求)時,應優先采用異步實現,以提升性能并避免阻塞線程。
- 避免不必要的
async修飾:如果方法沒有實際的異步工作(例如只是返回固定結果),應避免添加不必要的async修飾,直接返回任務(如Task.FromResult),以提高性能。
11.2 異步構造方法:工廠模式
問題
在某些場景下,我們需要在對象的構造過程中執行異步操作(例如加載配置文件、從遠程服務拉取數據等)。然而,C# 的構造函數不支持 async 或 await,因為構造函數本身無法返回 Task。
解決方案:異步工廠模式
一種優雅的解決方案是使用異步工廠方法模式,讓類自身提供一個靜態的異步工廠方法來創建和初始化實例。這種方法將初始化的異步邏輯與實例創建綁定在一起,確保只有在完成初始化后才可以訪問實例。
以下是一個完整的異步工廠模式代碼示例:
using System;
using System.Threading.Tasks;
class MyAsyncClass
{
// 私有構造器,防止直接實例化
private MyAsyncClass()
{
Console.WriteLine("Constructor called");
}
// 異步初始化方法,私有
private async Task<MyAsyncClass> InitializeAsync()
{
Console.WriteLine("Initializing asynchronously...");
await Task.Delay(1000); // 模擬異步操作
Console.WriteLine("Initialization complete");
return this;
}
// 靜態工廠方法
public static Task<MyAsyncClass> CreateAsync()
{
var instance = new MyAsyncClass();
return instance.InitializeAsync();
}
}
class Program
{
static async Task Main(string[] args)
{
Console.WriteLine("Creating instance...");
MyAsyncClass instance = await MyAsyncClass.CreateAsync();
Console.WriteLine("Instance created and ready to use!");
}
}
執行結果:
Creating instance...
Constructor called
Initializing asynchronously...
Initialization complete
Instance created and ready to use!
設計要點:
- 構造函數和
InitializeAsync方法為私有,防止直接調用。 - 通過靜態工廠方法
CreateAsync創建實例,并確保初始化完成后再返回對象。
常見問題與反例
反例:在構造器中啟動異步操作
class MyAsyncClass
{
public MyAsyncClass()
{
InitializeAsync();
}
// 錯誤:使用 async void
private async void InitializeAsync()
{
await Task.Delay(TimeSpan.FromSeconds(1)); // 模擬異步操作
}
}
問題:
- 未完成的實例: 構造函數返回時,實例的異步初始化尚未完成,調用方可能誤用未-準備好的實例。
- 異常無法捕獲:由于 async void 的特殊性,InitializeAsync 方法中拋出的異常無法通過構造函數外的 try-catch 捕獲。
- 不確定的狀態:調用方無法得知異步操作何時完成。
異步工廠模式的優點
-
安全性:
- 確保實例在初始化完成后才可用,避免未初始化實例被錯誤使用。
-
封裝性:
- 初始化邏輯被封裝在工廠方法中,調用方無需關心實現細節。
-
可維護性:
- 異步工廠模式強制調用者按正確的方式使用類型,減少潛在錯誤。
異步工廠模式的局限性
盡管異步工廠模式是解決異步構造問題的推薦方法,但在某些場景中可能會面臨以下局限性:
-
與依賴注入框架的不兼容:
-
大多數依賴注入(DI)框架(例如 ASP.NET Core 的 DI 容器)無法處理異步工廠方法。這是因為 DI 容器通常會直接調用構造器來創建對象,而無法等待異步工廠方法。
-
解決方案:
- 如果初始化是共享資源,可以使用惰性初始化(如
AsyncLazy)。 - 如果必須支持異步初始化,可以參考下一節的異步初始化模式。
- 如果初始化是共享資源,可以使用惰性初始化(如
-
-
代碼復雜性:
- 對于簡單的類型,異步工廠模式可能顯得過于復雜。如果初始化邏輯非常簡單,可以考慮讓調用方顯式調用異步初始化方法(盡管可能存在調用方忘記調用的問題)。
11.3 異步構造:異步初始化模式
問題
在某些情況下,無法使用工廠模式(參見 11.2 節),例如實例是通過以下方式創建的:
依賴注入(DI)容器:例如 ASP.NET Core 中的 DI。反射:如 Activator.CreateInstance。數據綁定。
此時我們需要一種機制來支持異步初始化,同時避免初始化過程中導致實例狀態不一致。
解決方案:異步初始化模式
異步初始化模式的核心思想是:
- 在構造器中啟動異步初始化,并將初始化的任務暴露為一個公共屬性
Initialization。 - 調用方可以通過檢查并等待
Initialization屬性,確保實例完成初始化后再使用。
代碼示例
1. 定義標記接口
為了規范所有需要異步初始化的類型,可以定義一個接口來強制實現 Initialization 屬性:
/// <summary>
/// 標記需要異步初始化的類型,并提供初始化任務
/// </summary>
public interface IAsyncInitialization
{
/// <summary>
/// 異步初始化任務
/// </summary>
Task Initialization { get; }
}
2. 實現基本類型
public class MyFundamentalType : IAsyncInitialization
{
public MyFundamentalType()
{
// 構造函數中啟動異步初始化
Initialization = InitializeAsync();
}
public Task Initialization { get; private set; }
private async Task InitializeAsync()
{
// 模擬異步初始化
await Task.Delay(TimeSpan.FromSeconds(1));
}
}
3. 使用場景
通過依賴注入框架或反射創建實例:
IMyFundamentalType instance = UltimateDIFactory.Create<IMyFundamentalType>();
if (instance is IAsyncInitialization instanceAsyncInit)
{
// 等待異步初始化完成
await instanceAsyncInit.Initialization;
}
4. 合成類型支持
合成類型可能依賴多個需要異步初始化的組件:
public class MyComposedType : IAsyncInitialization
{
private readonly IMyFundamentalType _fundamental;
public MyComposedType(IMyFundamentalType fundamental)
{
_fundamental = fundamental;
Initialization = InitializeAsync();
}
public Task Initialization { get; private set; }
private async Task InitializeAsync()
{
// 如果組件實現了異步初始化,則等待其完成
if (_fundamental is IAsyncInitialization fundamentalInit)
{
await fundamentalInit.Initialization;
}
// 執行自身的初始化邏輯
await Task.Delay(TimeSpan.FromSeconds(1));
}
}
5. 簡化多組件初始化
可通過輔助方法簡化對多個組件的初始化檢查:
public static class AsyncInitialization
{
public static Task WhenAllInitializedAsync(params object[] instances)
{
return Task.WhenAll(instances
.OfType<IAsyncInitialization>() // 篩選出需要異步初始化的實例
.Select(x => x.Initialization)); // 收集初始化任務
}
}
// 示例:合成類型依賴多個注入組件
private async Task InitializeAsync()
{
await AsyncInitialization.WhenAllInitializedAsync(_fundamental, _anotherType, _yetAnother);
// 自身的初始化邏輯
}
優缺點分析
優點
- 兼容性強:支持反射創建、依賴注入、數據綁定等場景。
- 異步初始化管理清晰:通過
Initialization屬性集中管理異步狀態。 - 靈活性:初始化可異步也可同步完成,允許根據具體情況實現。
缺點
- 暴露未初始化實例:與異步工廠模式不同,該模式允許調用方在初始化未完成時訪問實例,可能導致使用未完全準備好的實例。
- 復雜性:當組件依賴關系復雜時,需要額外代碼管理依賴的初始化順序和狀態。
最佳實踐
- 優先使用工廠模式:如果可能,盡量采用 11.2 節中的異步工廠模式,避免直接暴露未初始化的實例。
- 謹慎依賴異步初始化:減少對異步初始化的依賴,優先設計為延遲初始化。
- 輔助工具類簡化邏輯:對于復雜依賴關系,使用輔助方法(如
AsyncInitialization.WhenAllInitializedAsync)來減少冗余代碼。
11.4 異步屬性
問題
在實際開發中,可能會遇到需要將某個屬性轉換為異步操作的場景。比如,當屬性的 getter 方法需要執行異步操作時,如何正確地處理?然而,C# 中并沒有異步屬性的概念(即 async 屬性),也無法直接在屬性中使用 async 關鍵字。這種限制實際上是有意義的,因為屬性的設計語義是用于快速獲取數據,而不是啟動復雜的后臺操作。
以下是一個典型的錯誤示例(代碼無法編譯):
// 錯誤:嘗試將屬性變為異步
public int Data
{
async get
{
await Task.Delay(TimeSpan.FromSeconds(1));
return 13;
}
}
因此,當發現需要“異步屬性”時,實際上應該重新審視設計思路,根據場景選擇合適的實現方式。
解決方案:兩種設計選擇
在需要異步屬性的場景中,通常存在兩種需求:
- 每次訪問屬性時,重新啟動異步計算。
- 只進行一次異步計算并緩存結果,之后的訪問返回相同的值。
根據這兩種需求,可以分別采用以下解決方案:
方案 1:屬性值需要重復異步計算
在這種情況下,屬性的行為本質上是一個異步方法,因為每次訪問屬性時都會重新啟動異步操作。這種設計不適合用屬性實現,而應該改為顯式的異步方法。
示例:
// 使用異步方法代替屬性
public async Task<int> GetDataAsync()
{
await Task.Delay(TimeSpan.FromSeconds(1)); // 模擬異步操作
return 13;
}
調用代碼:
int value = await instance.GetDataAsync();
注意:雖然也可以通過屬性返回 Task<T>,如下所示:
// 返回 Task<T> 的屬性
public Task<int> Data
{
get { return GetDataAsync(); }
}
但是,這種設計會讓 API 產生誤導。調用方在讀取 Data 屬性時,可能會誤以為是普通的同步屬性,而不清楚其行為是異步的。因此,不推薦使用這種方式。
方案 2:異步計算值并緩存
如果屬性值只需要異步計算一次,并在后續訪問中返回相同的結果,可以使用異步延遲初始化的方式。這種方式非常適合用屬性來實現,并且符合屬性的語義。
示例:
public AsyncLazy<int> Data { get; }
private readonly AsyncLazy<int> _data = new AsyncLazy<int>(async () =>
{
await Task.Delay(TimeSpan.FromSeconds(1)); // 模擬異步操作
return 13;
});
調用代碼:
int value = await instance.Data; // 異步獲取值
在這個實現中,AsyncLazy 確保異步計算只會執行一次,之后的訪問直接返回緩存的結果。
反面示例和注意事項
在將同步屬性改為異步時,務必避免如下的反面示例:
private async Task<int> GetDataAsync()
{
await Task.Delay(TimeSpan.FromSeconds(1));
return 13;
}
// 錯誤:在屬性中調用異步方法并阻塞
public int Data
{
get { return GetDataAsync().Result; } // 阻塞等待異步操作完成
}
-
問題 1:性能和線程阻塞
使用.Result或.Wait()會阻塞當前線程,違背了異步編程的初衷,可能導致死鎖或性能問題。 -
問題 2:語義不清晰
屬性的語義應是快速訪問數據,而不是啟動復雜的操作。上述代碼在 API 設計上容易誤導調用方。
狀態屬性與異步語義
在將同步代碼轉換為異步代碼時,還需要特別注意屬性的狀態語義問題。以流操作中的 Stream.Position 為例:
- 在同步方法
Stream.Read或Stream.Write中,Position會在操作完成后更新,反映當前的流位置。 - 在異步方法
Stream.ReadAsync或Stream.WriteAsync中,Position的更新時機可能會產生歧義:- 是在異步操作完成后更新?
- 還是在調用
ReadAsync或WriteAsync方法時立即更新?
這些語義問題在異步化過程中需要特別考慮,并且應在 API 文檔中清晰說明。
完整代碼示例
以下是一個完整的示例,展示了異步方法和異步延遲初始化的兩種實現:
using System;
using System.Threading.Tasks;
class MyClass
{
// 異步方法:每次調用都會重新計算值
public async Task<int> GetDataAsync()
{
await Task.Delay(TimeSpan.FromSeconds(1)); // 模擬異步操作
return 13;
}
// 異步延遲初始化:值只計算一次并緩存
public AsyncLazy<int> Data { get; }
private readonly AsyncLazy<int> _data = new AsyncLazy<int>(async () =>
{
await Task.Delay(TimeSpan.FromSeconds(1)); // 模擬異步操作
return 13;
});
public MyClass()
{
Data = _data;
}
}
class Program
{
static async Task Main(string[] args)
{
var myClass = new MyClass();
// 使用異步方法
int value1 = await myClass.GetDataAsync();
Console.WriteLine($"Value from GetDataAsync: {value1}");
// 使用異步延遲初始化
int value2 = await myClass.Data;
Console.WriteLine($"Value from AsyncLazy<Data>: {value2}");
}
}
輸出:
Value from GetDataAsync: 13
Value from AsyncLazy<Data>: 13
通過這種方式,屬性與方法的語義更加明確,既滿足了異步計算的需求,又避免了設計上的歧義。
11.5 異步事件
問題
在設計異步事件時,如何跟蹤事件處理程序的完成情況?通常情況下,事件的觸發者不需要關心處理程序是否完成,這種情形多見于通知事件(如按鈕點擊)。但在某些情況下,觸發者需要等待所有處理程序完成(如生命周期事件),這被稱為命令事件。
一個常見的挑戰是,async void 異步處理程序無法被直接跟蹤,因為它不會返回 Task,因此我們需要一種替代方法來檢測異步處理程序的完成狀態。
解決方案:使用延遲管理器
為了解決這個問題,可以引入延遲管理器(DeferralManager),它可以跟蹤事件處理程序的延遲狀態:
- 處理程序分配延遲:延遲管理器為每個異步處理程序分配一個延遲對象,用于跟蹤異步處理的狀態。
- 延遲完成通知:當異步處理完成時,延遲對象會通知延遲管理器。
- 等待所有延遲完成:事件發送方可以等待延遲管理器跟蹤的所有延遲完成后再繼續執行。
實現步驟
1. 定義事件參數類型
為了支持延遲管理,事件參數類型需要擴展。可以通過實現 IDeferralSource 接口,并包含一個 DeferralManager 實例來管理延遲。
public class MyEventArgs : EventArgs, IDeferralSource
{
private readonly DeferralManager _deferrals = new DeferralManager();
// 獲取延遲對象
public IDisposable GetDeferral()
{
return _deferrals.DeferralSource.GetDeferral();
}
// 等待所有異步延遲完成
internal Task WaitForDeferralsAsync()
{
return _deferrals.WaitForDeferralsAsync();
}
}
在 MyEventArgs 中:
GetDeferral方法用于分配延遲對象。WaitForDeferralsAsync方法用于等待所有延遲完成。
2. 觸發異步事件
當觸發事件時,需要等待所有異步處理程序完成。可以使用以下代碼觸發事件:
public event EventHandler<MyEventArgs> MyEvent;
private async Task RaiseMyEventAsync()
{
// 獲取事件處理程序
EventHandler<MyEventArgs> handler = MyEvent;
if (handler == null)
return;
// 創建事件參數
var args = new MyEventArgs();
// 觸發事件
handler(this, args);
// 等待所有異步處理程序完成
await args.WaitForDeferralsAsync();
}
代碼解讀:
- 檢查是否有訂閱的處理程序。
- 創建
MyEventArgs實例。 - 調用事件處理程序。
- 調用
WaitForDeferralsAsync等待所有異步處理程序完成。
3. 異步事件處理程序
事件處理程序可以通過以下方式分配延遲,并在異步操作完成后通知延遲管理器:
async void AsyncHandler(object sender, MyEventArgs args)
{
using IDisposable deferral = args.GetDeferral(); // 分配延遲
await Task.Delay(TimeSpan.FromSeconds(2)); // 模擬異步操作
}
在這里,using 塊確保延遲對象在異步操作完成后被正確釋放,以此通知延遲管理器。
完整實現示例
以下是一個完整的代碼示例,展示如何使用延遲管理器實現異步事件:
using System;
using System.Threading.Tasks;
using Nito.AsyncEx; // 引入 Nito.AsyncEx 庫
// 定義事件參數類型,支持延遲管理
public class MyEventArgs : EventArgs, IDeferralSource
{
private readonly DeferralManager _deferrals = new DeferralManager();
// 獲取延遲對象
public IDisposable GetDeferral()
{
return _deferrals.DeferralSource.GetDeferral();
}
// 等待所有異步延遲完成
internal Task WaitForDeferralsAsync()
{
return _deferrals.WaitForDeferralsAsync();
}
}
public class MyEventSource
{
// 定義事件
public event EventHandler<MyEventArgs> MyEvent;
// 觸發事件并等待異步處理程序完成
public async Task RaiseMyEventAsync()
{
EventHandler<MyEventArgs> handler = MyEvent;
if (handler == null)
return;
// 創建事件參數
var args = new MyEventArgs();
// 觸發事件
handler(this, args);
// 等待所有異步處理程序完成
await args.WaitForDeferralsAsync();
}
}
public class Program
{
public static async Task Main(string[] args)
{
var source = new MyEventSource();
// 注冊異步事件處理程序
source.MyEvent += async (sender, e) =>
{
using IDisposable deferral = e.GetDeferral(); // 分配延遲
Console.WriteLine("Handler started.");
await Task.Delay(TimeSpan.FromSeconds(2)); // 模擬異步操作
Console.WriteLine("Handler completed.");
};
// 觸發事件并等待處理程序完成
Console.WriteLine("Raising event...");
await source.RaiseMyEventAsync();
Console.WriteLine("Event processing completed.");
}
}
輸出:
Raising event...
Handler started.
Handler completed.
Event processing completed.
通知事件 vs 命令事件
.NET 中的事件可以按語義分為兩種:
-
通知事件:
- 用于通知訂閱方某些情況發生。
- 通常是單向的,事件發送方并不關心訂閱方是否完成處理。
- 特點:無需額外代碼支持異步處理程序。例如,按鈕單擊事件屬于通知事件。
- 處理方式:異步處理程序可以是
async void,發送方不需要等待其完成。
-
命令事件:
- 用于觸發某些功能,發送方需要等待訂閱方完成處理后才能繼續。
- 特點:發送方需要檢測訂閱方的完成狀態,異步處理程序需要顯式等待。
- 處理方式:需要引入延遲機制(如
DeferralManager),以跟蹤異步處理狀態。
最佳實踐
-
延遲的作用范圍:
延遲機制主要針對命令事件,因為命令事件需要等待處理程序完成。而對于通知事件,這種機制是不必要的。 -
線程安全性:
事件參數的類型應該是線程安全的。最簡單的實現方式是使事件參數不可變(所有屬性均為只讀)。 -
Nito.AsyncEx:
使用DeferralManager是一種簡潔的擴展方式,可從 NuGet 包Nito.AsyncEx中直接獲取。 -
異步事件的設計原則:
- 如果事件是通知性質的,無需額外代碼支持異步處理程序。
- 如果事件是命令性質的,需要明確等待處理程序完成,且需要文檔清晰說明其語義。
11.6 異步釋放
問題
在某些類型中,需要在釋放(Dispose)資源時處理異步操作。如何實現異步資源釋放,同時確保語義清晰且與現有的 .NET 生態系統兼容?
解決方案
.NET 提供了兩種常見模式來實現資源釋放:
- 釋放作為取消:將釋放視為對所有正在進行操作的取消請求。
- 異步釋放:使用異步語義,在釋放資源時等待異步操作完成。
1. 將釋放視為取消
這種模式適用于需要在釋放資源時取消現有操作的場景,例如 HttpClient。通過使用 CancellationTokenSource 來取消當前操作,避免資源占用。
實現代碼:
class MyClass : IDisposable
{
private readonly CancellationTokenSource _disposeCts = new CancellationTokenSource();
public async Task<int> CalculateValueAsync(CancellationToken cancellationToken = default)
{
using var combinedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, _disposeCts.Token);
await Task.Delay(TimeSpan.FromSeconds(2), combinedCts.Token);
return 13;
}
public void Dispose()
{
_disposeCts.Cancel();
}
}
用法:
async Task UseMyClassAsync()
{
Task<int> task;
using (var resource = new MyClass())
{
task = resource.CalculateValueAsync();
}
// 在 Dispose 后調用 await 將拋出 OperationCanceledException
var result = await task;
}
2. 異步釋放(IAsyncDisposable)
異步釋放在 C# 8.0 和 .NET Core 3.0 中引入,通過 IAsyncDisposable 接口和 DisposeAsync 方法實現。在釋放資源時,可以等待異步操作完成。
實現代碼:
class MyClass : IAsyncDisposable
{
public async ValueTask DisposeAsync()
{
// 模擬異步釋放操作
await Task.Delay(TimeSpan.FromSeconds(2));
}
}
用法:
使用 await using 語法來異步釋放資源:
await using (var myClass = new MyClass())
{
// 使用資源
}
// 此處調用并等待 DisposeAsync
如果需要避免捕獲同步上下文,可以使用 ConfigureAwait(false):
var myClass = new MyClass();
await using (myClass.ConfigureAwait(false))
{
// 使用 myClass 的邏輯
}
// 此處調用 DisposeAsync 并避免上下文捕獲
注意:DisposeAsync 返回 ValueTask 而非 Task。這可以減少內存分配成本,但兩者都支持標準的 async/await 語法。
兩種模式的比較
| 模式 | 優勢 | 劣勢 |
|---|---|---|
| 釋放作為取消 | 簡單且廣泛兼容,適用于多數場景。 | 無法等待未完成的操作。 |
| 異步釋放 | 支持異步操作完成,適用于需要嚴格釋放的資源。 | 實現復雜,依賴 C# 8.0 及更高版本。 |
在某些場景下,兩種模式可以結合使用:
- 如果客戶端使用
Dispose,表示取消正在進行的操作。 - 如果客戶端使用
DisposeAsync,則等待所有操作完成并釋放資源。
組合實現代碼:
class MyClass : IDisposable, IAsyncDisposable
{
private readonly CancellationTokenSource _disposeCts = new CancellationTokenSource();
public async Task<int> CalculateValueAsync(CancellationToken cancellationToken = default)
{
using var combinedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, _disposeCts.Token);
await Task.Delay(TimeSpan.FromSeconds(2), combinedCts.Token);
return 13;
}
public void Dispose()
{
_disposeCts.Cancel(); // 取消操作
}
public async ValueTask DisposeAsync()
{
_disposeCts.Cancel(); // 取消操作
await Task.Delay(TimeSpan.FromSeconds(2)); // 模擬異步釋放資源
}
}

浙公網安備 33010602011771號