第十一章:C#异步函数与面向对象编程的融合

第十一章:C#异步函数与面向对象编程的融合


在现代软件开发中,异步编程已经成为提升应用程序性能和用户体验的关键技术。然而,C# 的异步特性是函数式编程范式的产物,与传统面向对象编程存在本质上的差异。当需要将异步操作融入面向对象的设计中时,你可能会遇到许多独特的挑战,例如接口方法的异步化、构造函数中的异步逻辑处理、异步属性与事件的设计等。

11.1 异步接口及继承

问题

在接口或基类中定义一个方法时,如何将其异步化?

解决方案

核心理念是:async 是一个实现细节,不能直接用于接口或抽象方法的定义,但可以通过返回 TaskTask<T> 来定义异步的签名。

  • 接口方法或抽象方法:直接返回 TaskTask<T>,表示其实现可能是异步的。
  • 实现方法:在实现时使用 async 关键字完成具体的异步操作。

这样,异步接口的设计兼容同步实现和异步实现,也符合灵活性和规范性的需求。

代码示例

  1. 定义接口

接口通过返回 TaskTask<T> 的方式定义“异步方法”。这样的方法本质上是“可等待的”,可以通过 await 使用:

interface IMyAsyncInterface
{
    Task<int> CountBytesAsync(HttpClient client, string url);
}
  1. 异步实现
    类实现接口,并在方法体内使用 async/await 执行异步操作:
class MyAsyncClass : IMyAsyncInterface
{
    public async Task<int> CountBytesAsync(HttpClient client, string url)
    {
        var bytes = await client.GetByteArrayAsync(url);// 异步操作
        return bytes.Length;
    }
}
  1. 消费异步接口

在使用该接口时,可以直接通过 await 调用其方法:

async Task UseMyInterfaceAsync(HttpClient client, IMyAsyncInterface service)
{
    var result = await service.CountBytesAsync(client, "http://example.com");
    Console.WriteLine(result);
}
  1. 同步实现

为了方便测试或兼容需求,接口的实现方法可以同步返回结果,而不需要实际执行异步操作:

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;
   });
}

小结

  1. 接口方法不能包含 async 关键字

    • async 是方法实现的一部分,用于告诉编译器生成状态机来处理异步操作,而接口和抽象类仅定义方法的签名,不涉及实现细节。因此,接口和抽象类的方法不能使用 async
  2. 异步接口的本质

    • 异步接口的核心在于返回类型是 TaskTask<T>,而不是方法本身是否使用 async
    • 调用方只需知道方法返回可等待的任务,而无需关心具体实现是异步的还是同步的。
  3. 异步任务的本质

    • 一个 TaskTask<T> 代表一项任务,可能是未完成的任务(将在未来完成)或已完成的任务(立即返回结果)。
    • 通过这种机制,调用方可以统一使用 await 等待任务的完成,而不需要感知任务的具体状态。
  4. 实现的灵活性

    • 接口方法可以通过真正的异步操作(如 asyncawait)实现,也可以通过同步方式(如 Task.FromResult)实现。
    • 这种灵活性允许在不同场景下选择合适的实现策略:
      • 如果存在实际的异步操作(如 I/O、网络请求),应采用真正的异步实现。
      • 如果不需要异步操作,可以返回一个已完成的任务(例如 Task.FromResult)。
  5. 异步化策略

    • 优先异步:当方法涉及 I/O 操作(如文件读写、网络请求)时,应优先采用异步实现,以提升性能并避免阻塞线程。
    • 避免不必要的 async 修饰:如果方法没有实际的异步工作(例如只是返回固定结果),应避免添加不必要的 async 修饰,直接返回任务(如 Task.FromResult),以提高性能。

11.2 异步构造方法:工厂模式

问题

在某些场景下,我们需要在对象的构造过程中执行异步操作(例如加载配置文件、从远程服务拉取数据等)。然而,C# 的构造函数不支持 asyncawait,因为构造函数本身无法返回 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)); // 模拟异步操作
    }
}

问题:

  1. 未完成的实例: 构造函数返回时,实例的异步初始化尚未完成,调用方可能误用未-准备好的实例。
  2. 异常无法捕获:由于 async void 的特殊性,InitializeAsync 方法中抛出的异常无法通过构造函数外的 try-catch 捕获。
  3. 不确定的状态:调用方无法得知异步操作何时完成。

异步工厂模式的优点

  1. 安全性

    • 确保实例在初始化完成后才可用,避免未初始化实例被错误使用。
  2. 封装性

    • 初始化逻辑被封装在工厂方法中,调用方无需关心实现细节。
  3. 可维护性

    • 异步工厂模式强制调用者按正确的方式使用类型,减少潜在错误。

异步工厂模式的局限性

尽管异步工厂模式是解决异步构造问题的推荐方法,但在某些场景中可能会面临以下局限性:

  1. 与依赖注入框架的不兼容

    • 大多数依赖注入(DI)框架(例如 ASP.NET Core 的 DI 容器)无法处理异步工厂方法。这是因为 DI 容器通常会直接调用构造器来创建对象,而无法等待异步工厂方法。

    • 解决方案

      • 如果初始化是共享资源,可以使用惰性初始化(如 AsyncLazy)。
      • 如果必须支持异步初始化,可以参考下一节的异步初始化模式
  2. 代码复杂性

    • 对于简单的类型,异步工厂模式可能显得过于复杂。如果初始化逻辑非常简单,可以考虑让调用方显式调用异步初始化方法(尽管可能存在调用方忘记调用的问题)。

11.3 异步构造:异步初始化模式

问题

在某些情况下,无法使用工厂模式(参见 11.2 节),例如实例是通过以下方式创建的:

  • 依赖注入(DI)容器:例如 ASP.NET Core 中的 DI。
  • 反射:如 Activator.CreateInstance。
  • 数据绑定

此时我们需要一种机制来支持异步初始化,同时避免初始化过程中导致实例状态不一致。

解决方案:异步初始化模式

异步初始化模式的核心思想是:

  1. 在构造器中启动异步初始化,并将初始化的任务暴露为一个公共属性 Initialization
  2. 调用方可以通过检查并等待 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);
    // 自身的初始化逻辑
}

优缺点分析

优点

  1. 兼容性强:支持反射创建、依赖注入、数据绑定等场景。
  2. 异步初始化管理清晰:通过 Initialization 属性集中管理异步状态。
  3. 灵活性:初始化可异步也可同步完成,允许根据具体情况实现。

缺点

  1. 暴露未初始化实例:与异步工厂模式不同,该模式允许调用方在初始化未完成时访问实例,可能导致使用未完全准备好的实例。
  2. 复杂性:当组件依赖关系复杂时,需要额外代码管理依赖的初始化顺序和状态。

最佳实践

  1. 优先使用工厂模式:如果可能,尽量采用 11.2 节中的异步工厂模式,避免直接暴露未初始化的实例。
  2. 谨慎依赖异步初始化:减少对异步初始化的依赖,优先设计为延迟初始化。
  3. 辅助工具类简化逻辑:对于复杂依赖关系,使用辅助方法(如 AsyncInitialization.WhenAllInitializedAsync)来减少冗余代码。

11.4 异步属性

问题

在实际开发中,可能会遇到需要将某个属性转换为异步操作的场景。比如,当属性的 getter 方法需要执行异步操作时,如何正确地处理?然而,C# 中并没有异步属性的概念(即 async 属性),也无法直接在属性中使用 async 关键字。这种限制实际上是有意义的,因为属性的设计语义是用于快速获取数据,而不是启动复杂的后台操作。

以下是一个典型的错误示例(代码无法编译):

// 错误:尝试将属性变为异步
public int Data
{
    async get
    {
        await Task.Delay(TimeSpan.FromSeconds(1));
        return 13;
    }
}

因此,当发现需要“异步属性”时,实际上应该重新审视设计思路,根据场景选择合适的实现方式。

解决方案:两种设计选择

在需要异步属性的场景中,通常存在两种需求:

  1. 每次访问属性时,重新启动异步计算。
  2. 只进行一次异步计算并缓存结果,之后的访问返回相同的值。

根据这两种需求,可以分别采用以下解决方案:

方案 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.ReadStream.Write 中,Position 会在操作完成后更新,反映当前的流位置。
  • 在异步方法 Stream.ReadAsyncStream.WriteAsync 中,Position 的更新时机可能会产生歧义:
    • 是在异步操作完成后更新?
    • 还是在调用 ReadAsyncWriteAsync 方法时立即更新?

这些语义问题在异步化过程中需要特别考虑,并且应在 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. 处理程序分配延迟:延迟管理器为每个异步处理程序分配一个延迟对象,用于跟踪异步处理的状态。
  2. 延迟完成通知:当异步处理完成时,延迟对象会通知延迟管理器。
  3. 等待所有延迟完成:事件发送方可以等待延迟管理器跟踪的所有延迟完成后再继续执行。

实现步骤

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();
}

代码解读:

  1. 检查是否有订阅的处理程序。
  2. 创建 MyEventArgs 实例。
  3. 调用事件处理程序。
  4. 调用 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 中的事件可以按语义分为两种:

  1. 通知事件

    • 用于通知订阅方某些情况发生。
    • 通常是单向的,事件发送方并不关心订阅方是否完成处理。
    • 特点:无需额外代码支持异步处理程序。例如,按钮单击事件属于通知事件。
    • 处理方式:异步处理程序可以是 async void,发送方不需要等待其完成。
  2. 命令事件

    • 用于触发某些功能,发送方需要等待订阅方完成处理后才能继续。
    • 特点:发送方需要检测订阅方的完成状态,异步处理程序需要显式等待。
    • 处理方式:需要引入延迟机制(如 DeferralManager),以跟踪异步处理状态。

最佳实践

  1. 延迟的作用范围
    延迟机制主要针对命令事件,因为命令事件需要等待处理程序完成。而对于通知事件,这种机制是不必要的。

  2. 线程安全性
    事件参数的类型应该是线程安全的。最简单的实现方式是使事件参数不可变(所有属性均为只读)。

  3. Nito.AsyncEx
    使用 DeferralManager 是一种简洁的扩展方式,可从 NuGet 包 Nito.AsyncEx 中直接获取。

  4. 异步事件的设计原则

    • 如果事件是通知性质的,无需额外代码支持异步处理程序。
    • 如果事件是命令性质的,需要明确等待处理程序完成,且需要文档清晰说明其语义。

11.6 异步释放

问题

在某些类型中,需要在释放(Dispose)资源时处理异步操作。如何实现异步资源释放,同时确保语义清晰且与现有的 .NET 生态系统兼容?

解决方案

.NET 提供了两种常见模式来实现资源释放:

  1. 释放作为取消:将释放视为对所有正在进行操作的取消请求。
  2. 异步释放:使用异步语义,在释放资源时等待异步操作完成。

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)); // 模拟异步释放资源
    }
}
posted @ 2024-12-09 16:14  平元兄  阅读(24)  评论(0编辑  收藏  举报