第十一章: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)); // 模拟异步释放资源 } }
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· DeepSeek “源神”启动!「GitHub 热点速览」
· C# 集成 DeepSeek 模型实现 AI 私有化(本地部署与 API 调用教程)
· DeepSeek R1 简明指南:架构、训练、本地部署及硬件要求
· NetPad:一个.NET开源、跨平台的C#编辑器