《Concurrency in C# Cookbook》--- 读书随记(6)

CHAPTER 11 Functional-Friendly OOP

《Concurrency in C# Cookbook》
Asynchronous, Parallel, and Multithreaded Programming

Author: Stephen Cleary

如果需要电子书的小伙伴,可以留下邮箱,看到了会发送的

使用异步的主要突破是,在异步编程时仍然可以按过程思考。这使得异步方法更容易编写和理解。然而,异步代码在本质上仍然是函数式的,当人们试图将异步方法强加到经典的面向对象设计中时,这会导致一些问题。本章中处理了异步代码与面向对象程序设计冲突的摩擦点

11.1 Async Interfaces and Inheritance

Problem

接口或基类中有一个方法需要进行异步处理

Solution

理解这个问题及其解决方案的关键是要认识到async是一个实现细节。async关键字只能应用于具有实现的方法; 不可能应用于抽象方法或接口方法(除非它们具有默认实现)。但是,您可以定义一个具有与async方法相同签名的方法,只是没有async关键字

请记住,类型是awaitable的,而不是方法。您可以await方法返回的 Task,无论该方法是否使用async实现。因此,接口或抽象方法只需返回一个 Task (或 Task < T >) ,并且该方法的返回值是awaitable的

interface IMyAsyncInterface
{
    Task<int> CountBytesAsync(HttpClient client, string url);
}

class MyAsyncClass : IMyAsyncInterface
{
    public async Task<int> CountBytesAsync(HttpClient client, string url)
    {
        var bytes = await client.GetByteArrayAsync(url);
        return bytes.Length;
    }
}

async Task UseMyInterfaceAsync(HttpClient client, IMyAsyncInterface service)
{
    var result = await service.CountBytesAsync(client, "http://www.example.com");
    Trace.WriteLine(result);
}

异步方法签名只意味着实现可能是异步的。如果实际的实现没有真正的异步工作要做,那么实际的实现可能是同步的

Discussion

随着异步方法变得越来越常见,接口和基类上的异步方法也将变得越来越常见。如果您记住可等待的是返回类型(而不是方法) ,并且异步方法定义可以异步或同步实现,那么使用它们并不难

11.2 Async Construction: Factories

Problem

您正在编写的类型需要在其构造函数中完成一些异步工作

Solution

构造函数不能是异步的,也不能使用 await 关键字。在构造函数中等待当然是有用的,但这将极大地改变 C # 语言

一种可能性是将构造函数与异步初始化方法配对,这样就可以像这样使用类型

var instance = new MyAsyncClass();
await instance.InitializeAsync();

这种方法有一些缺点。很容易忘记调用 InitializeAsync 方法,并且实例在构造之后不能立即使用

更好的解决方案是使类型成为自己的工厂

class MyAsyncClass
{
    private MyAsyncClass()
    {
    }

    private async Task<MyAsyncClass> InitializeAsync()
    {
        await Task.Delay(TimeSpan.FromSeconds(1));
        return this;
    }

    public static Task<MyAsyncClass> CreateAsync()
    {
        var result = new MyAsyncClass();
        return result.InitializeAsync();
    }
}

Discussion

遗憾的是,这种方法在某些情况下不起作用,特别是当代码使用依赖注入提供程序时。没有主要的依赖注入或控制反转库使用异步代码。如果您发现自己处于这些场景之一,那么您可以考虑以下几种选择

如果您正在创建的实例实际上是一个共享资源,那么您可以使用异步延迟类型,否则,可以使用异步初始化模式

11.3 Async Construction: The Asynchronous Initialization Pattern

Problem

你正在编写的类型需要在它的构造函数中完成一些异步工作,但是你不能使用上一节的异步工厂模式 ,因为实例是通过反射创建的(例如,依赖注入/控制反转库、数据绑定、 Activator。CreateInstance 等)

Solution

当遇到这种情况时,必须返回一个未初始化的实例,但是可以通过应用一种通用模式(异步初始化模式)来缓解这种情况。每个需要异步初始化的类型都应该定义一个属性

Task Initialization { get; }

我通常喜欢在需要异步初始化的类型的标记接口中定义它

/// <summary>
/// Marks a type as requiring asynchronous initialization
/// and provides the result of that initialization.
/// </summary>
public interface IAsyncInitialization
{
    /// <summary>
    /// The result of the asynchronous initialization of this instance.
    /// </summary>
    Task Initialization { get; }
}

实现此模式时,应在构造函数中启动初始化(并分配 Initialization 属性)。异步初始化的结果(包括任何异常)通过该 Initialization 属性公开。下面是一个使用异步初始化的简单类型的示例实现

class MyFundamentalType : IMyFundamentalType, IAsyncInitialization
{
    public MyFundamentalType()
    {
        Initialization = InitializeAsync();
    }

    public Task Initialization { get; private set; }

    private async Task InitializeAsync()
    {
        // Asynchronously initialize this instance.
        await Task.Delay(TimeSpan.FromSeconds(1));
    }
}

如果您使用的是依赖注入/控制反转库,您可以使用以下代码创建和初始化这种类型的实例

IMyFundamentalType instance = UltimateDIFactory.Create<IMyFundamentalType>();
var instanceAsyncInit = instance as IAsyncInitialization;
if (instanceAsyncInit != null)
    await instanceAsyncInit.Initialization;

可以扩展此模式以允许使用异步初始化组合类型。在下面的示例中,定义了另一个依赖于 IMyFundamental 的类型

class MyComposedType : IMyComposedType, IAsyncInitialization
{
    private readonly IMyFundamentalType _fundamental;

    public MyComposedType(IMyFundamentalType fundamental)
    {
        _fundamental = fundamental;
        Initialization = InitializeAsync();
    }

    public Task Initialization { get; private set; }

    private async Task InitializeAsync()
    {
        // Asynchronously wait for the fundamental instance to initialize,
        // if necessary.
        var fundamentalAsyncInit = _fundamental as IAsyncInitialization;
        if (fundamentalAsyncInit != null)
            await fundamentalAsyncInit.Initialization;
        // Do our own initialization (synchronous or asynchronous).
        ...
    }
}

组合类型在继续进行初始化之前等待其所有组件进行初始化。要遵循的规则是,每个组件都应该在 InitializeAsync 结束时初始化。这样可以确保将所有依赖类型作为组合初始化的一部分进行初始化。组件初始化中的任何异常都会传播到组合类型的初始化中

Discussion

如果可以的话,我建议使用异步工厂或异步惰性初始模式来代替这个解决方案。这些是最好的方法,因为您永远不会公开未初始化的实例。但是,如果你的实例是通过依赖注入/控制反转、数据绑定等方式创建的,那么你就必须公开一个未初始化的实例,在这种情况下,我建议使用这个异步初始化模式

用于检查实例是否实现 IAsyncInitialization 并对其进行初始化的代码有些笨拙,当您拥有一个依赖于大量组件的组合类型时,这种情况会更加严重。创建一个可用于简化代码的 helper 方法非常简单

public static class AsyncInitialization
{
    public static Task WhenAllInitializedAsync(params object[] instances)
    {
        return Task.WhenAll(instances
            .OfType<IAsyncInitialization>()
            .Select(x => x.Initialization));
    }
}

您可以调用 InitializeAllAsync 并传递任何需要初始化的实例; 该方法将忽略未实现 IAsyncInitialization 的实例。依赖于三个注入实例的组合类型的初始化代码如下所示

private async Task InitializeAsync()
{
    // Asynchronously wait for all 3 instances to initialize, if necessary.
    await AsyncInitialization.WhenAllInitializedAsync(_fundamental,
        _anotherType, _yetAnother);
    // Do our own initialization (synchronous or asynchronous).
    ...
}

11.4 Async Properties

Problem

您有一个属性,您想使异步。该属性不用于数据绑定

Solution

这是在将现有代码转换为使用异步时经常出现的问题; 在这种情况下,您有一个属性,其 getter 调用的方法现在是异步的。然而,并不存在所谓的“异步属性”不可能在属性中使用 async 关键字,这是一件好事。属性 getter 应该返回当前值; 它们不应该启动后台操作

// What we think we want (does not compile).
public int Data
{
    async get
    {
        await Task.Delay(TimeSpan.FromSeconds(1));
        return 13;
    }
}

当您发现您的代码需要一个“异步属性”时,您的代码真正需要的是一些不同的东西。解决方案取决于您的属性值是需要计算一次还是多次

如果您的“异步属性”需要在每次读取时启动一个新的(异步)计算,那么它就不是一个属性; 它是一个伪装的方法。如果在将同步代码转换为异步代码时遇到了这种情况,那么是时候承认原始设计实际上是不正确的; 该属性应该一直是一个方法:

// As an asynchronous method.
public async Task<int> GetDataAsync()
{
    await Task.Delay(TimeSpan.FromSeconds(1));
    return 13;
}

可以直接从属性返回 Task < int > ,如下面的代码所示

// This "async property" is an asynchronous method.
// This "async property" is a Task-returning property.
public Task<int> Data
{
    get { return GetDataAsync(); }
}

private async Task<int> GetDataAsync()
{
    await Task.Delay(TimeSpan.FromSeconds(1));
    return 13;
}

然而,我不推荐这种方法。如果对属性的每次访问都将启动一个新的异步操作,那么这个“属性”实际上应该是一个方法。它是一个异步方法的事实清楚地表明,每次都会启动一个新的异步操作,因此 API 并没有误导。上两节确实使用Task返回属性,但是这些属性作为一个整体应用于实例; 它们不会在每次读取时启动一个新的异步操作

其他时候,您希望属性只启动单个(异步)计算并缓存结果值,以供将来使用。在这种情况下,可以使用异步惰性初始模式

// As a cached value
public AsyncLazy<int> Data
{
    get { return _data; }
}

private readonly AsyncLazy<int> _data =
        new AsyncLazy<int>(async () =>
        {
            await Task.Delay(TimeSpan.FromSeconds(1));
            return 13;
        });

代码只执行一次异步计算,然后将相同的值返回给所有调用方。调用代码如下所示

int value = await instance.Data;

11.5 Async Events

Problem

您有一个需要与可能是异步的处理程序一起使用的事件,并且需要检测事件处理程序是否已经完成。请注意,在引发事件时这种情况很少见; 通常,在引发事件时,您并不关心处理程序何时完成

11.6 Async Disposal

Problem

您的类型具有异步操作,但是还需要启用对其资源的处置

Solution

现有操作有两个常见选项: 可以将disposal视为应用于所有现有操作的取消请求,也可以实现实际的异步disposal

将 disposal 视为取消操作在 Windows 上具有历史优先级,文件流和套接字等类型在关闭时会取消任何现有的读或写操作。通过定义您自己的私有 CellationTokenSource 并将该令牌传递给您的内部操作,您可以在 NET 中执行非常类似的操作。使用以下代码,Dispose 将取消操作,但不会等待这些操作完成

class MyClass : IDisposable
{
    private readonly CancellationTokenSource _disposeCts =
        new CancellationTokenSource();

    public async Task<int> CalculateValueAsync()
    {
        await Task.Delay(TimeSpan.FromSeconds(2), _disposeCts.Token);
        return 13;
    }

    public void Dispose()
    {
        _disposeCts.Cancel();
    }
}

代码显示了销毁的基本模式。在现实世界的应用程序中,你应该检查对象是否已经处理掉,并且允许用户提供自己的取消令牌

public async Task<int> CalculateValueAsync(CancellationToken cancellationToken)
{
    using CancellationTokenSource combinedCts = CancellationTokenSource
        .CreateLinkedTokenSource(cancellationToken, _disposeCts.Token);
    await Task.Delay(TimeSpan.FromSeconds(2), combinedCts.Token);
    return 13;
}

但是,其他类型需要知道所有操作何时完成。对于这些类型,您需要某种异步 disposal(IAsyncDisposable)

class MyClass : IAsyncDisposable
{
    public async ValueTask DisposeAsync()
    {
        await Task.Delay(TimeSpan.FromSeconds(2));
    }
}

实现 IAsyncDisposable 的类型通常由 await using

await using (var myClass = new MyClass())
{
    ...
} // DisposeAsync is invoked (and awaited) here.
posted @   huang1993  阅读(30)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· DeepSeek “源神”启动!「GitHub 热点速览」
· C# 集成 DeepSeek 模型实现 AI 私有化(本地部署与 API 调用教程)
· DeepSeek R1 简明指南:架构、训练、本地部署及硬件要求
· NetPad:一个.NET开源、跨平台的C#编辑器
点击右上角即可分享
微信分享提示