《Concurrency in C# Cookbook》--- 读书随记(6)
CHAPTER 11 Functional-Friendly OOP
《Concurrency in C# Cookbook》
Asynchronous, Parallel, and Multithreaded ProgrammingAuthor: 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.
【推荐】编程新体验,更懂你的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#编辑器