1. await 操作的本质与可等待模式
在 C# 的异步编程模型中,await 关键字后面跟随的表达式并不一定必须是返回 Task 的函数。这是一个常见的误解,实际上 await 可以作用于任何实现了"可等待模式"(Awaitable Pattern)的类型。让我们深入剖析这个机制。
1.1 可等待模式的三要素
要让一个类型支持 await 操作,需要满足三个核心要求:
- 类型必须包含 GetAwaiter() 方法
- GetAwaiter() 返回的对象必须实现 INotifyCompletion 或 ICriticalNotifyCompletion 接口
- 返回的等待器对象必须具有:
- bool IsCompleted { get; } 属性
- GetResult() 方法
- OnCompleted(Action continuation) 方法
这种设计模式非常灵活,允许开发者自定义异步操作的行为,而不仅限于 Task 和 Task 。
1.2 常见可等待类型
除了 Task 和 Task 之外,.NET 中还有其他内置类型支持 await:
- ValueTask 和 ValueTask :轻量级任务,避免堆分配
- YieldAwaitable:用于 yield return 的异步迭代
- ConfiguredTaskAwaitable:用于配置任务等待行为
- 各种异步流和通道类型
2. 自定义可等待类型的实现
2.1 基础实现方案
下面是一个最简单的自定义可等待类型的实现示例:
public class MyAwaitable { public MyAwaiter GetAwaiter() => new MyAwaiter(); public class MyAwaiter : INotifyCompletion { public bool IsCompleted { get; private set; } public void OnCompleted(Action continuation) { Task.Run(() => { Thread.Sleep(1000); // 模拟异步操作 IsCompleted = true; continuation(); }); } public void GetResult() { } } }使用方式:
await new MyAwaitable();2.2 带返回值的实现
如果需要从 await 表达式获取结果,可以这样实现:
public class MyAwaitable<T> { private readonly Func<T> _operation; public MyAwaitable(Func<T> operation) => _operation = operation; public MyAwaiter GetAwaiter() => new MyAwaiter(_operation); public class MyAwaiter : INotifyCompletion { private readonly Func<T> _operation; private T _result; public MyAwaiter(Func<T> operation) => _operation = operation; public bool IsCompleted { get; private set; } public void OnCompleted(Action continuation) { Task.Run(() => { _result = _operation(); IsCompleted = true; continuation(); }); } public T GetResult() => _result; } }使用方式:
var result = await new MyAwaitable<int>(() => 42);3. 高级应用场景
3.1 基于 Task 的封装
在实际开发中,我们通常会基于 Task 进行封装,而不是从头实现:
public class OptimizedAwaitable<T> { private readonly Task<T> _task; public OptimizedAwaitable(Task<T> task) => _task = task; public TaskAwaiter<T> GetAwaiter() => _task.GetAwaiter(); // 可以添加各种配置方法 public OptimizedAwaitable<T> ConfigureAwait(bool continueOnCapturedContext) { _task.ConfigureAwait(continueOnCapturedContext); return this; } }3.2 性能优化技巧
- 避免不必要的堆分配:对于高频调用的异步操作,考虑使用结构体(struct)实现等待器
- 合理使用 IsCompleted:如果可以立即完成,直接返回结果避免状态机开销
- 注意线程上下文:正确处理同步上下文切换
4. 常见问题与解决方案
4.1 为什么我的自定义类型不能被 await?
检查清单:
- 是否实现了 GetAwaiter() 方法?
- 返回的等待器是否实现了 INotifyCompletion?
- 是否提供了 IsCompleted 属性和 GetResult() 方法?
- OnCompleted 是否正确调用了 continuation?
4.2 如何处理异常?
在 GetResult() 方法中抛出异常,await 表达式会捕获这些异常:
public T GetResult() { if (!IsCompleted) throw new InvalidOperationException("Operation not completed"); if (_exception != null) throw _exception; return _result; }4.3 如何取消异步操作?
可以结合 CancellationToken 实现:
public class CancellableAwaitable<T> { private readonly Func<CancellationToken, T> _operation; private readonly CancellationToken _cancellationToken; public CancellableAwaitable(Func<CancellationToken, T> operation, CancellationToken cancellationToken = default) { _operation = operation; _cancellationToken = cancellationToken; } // 实现等待器时检查取消令牌 public void OnCompleted(Action continuation) { if (_cancellationToken.IsCancellationRequested) { _exception = new OperationCanceledException(_cancellationToken); continuation(); return; } // ...其余实现 } }5. 最佳实践与性能考量
5.1 何时应该自定义可等待类型?
考虑自定义实现的场景:
- 需要特殊资源管理的异步操作
- 需要优化高频调用的轻量级异步操作
- 需要与特定硬件或外部系统集成
- 需要实现特殊的调度或同步行为
5.2 性能优化建议
- 使用 ValueTask:对于可能同步完成的操作,优先考虑 ValueTask
- 避免过度封装:如果没有特殊需求,直接使用 Task
- 注意状态机生成:复杂的 await 表达式会导致更大的状态机
- 合理使用 ConfigureAwait:在库代码中通常应该使用 ConfigureAwait(false)
5.3 调试技巧
- 检查状态机:使用反编译工具查看编译器生成的状态机代码
- 跟踪异步流:使用调试器的异步调用堆栈视图
- 记录时间点:在关键方法中添加日志记录
6. 实际案例:自定义延迟执行
下面是一个实用的延迟执行实现:
public struct DelayAwaitable { private readonly int _milliseconds; public DelayAwaitable(int milliseconds) => _milliseconds = milliseconds; public DelayAwaiter GetAwaiter() => new DelayAwaiter(_milliseconds); public struct DelayAwaiter : INotifyCompletion { private readonly int _milliseconds; private Timer _timer; public DelayAwaiter(int milliseconds) => (_milliseconds, _timer) = (milliseconds, null); public bool IsCompleted => _milliseconds <= 0; public void OnCompleted(Action continuation) { if (_milliseconds <= 0) { continuation(); return; } _timer = new Timer(_ => { _timer.Dispose(); continuation(); }, null, _milliseconds, Timeout.Infinite); } public void GetResult() { } } }使用方式:
await new DelayAwaitable(1000); // 延迟1秒这个实现比 Task.Delay 更轻量,因为它使用了 Timer 而不是 Task。
7. 异步模式与可等待模式的区别
7.1 异步模式(Asynchronous Pattern)
传统的异步模式基于 Begin/End 方法对:
IAsyncResult BeginXXX(..., AsyncCallback callback, object state); ResultType EndXXX(IAsyncResult asyncResult);7.2 基于任务的异步模式(TAP)
现代 .NET 推荐的模式,返回 Task 或 Task :
Task<ResultType> XXXAsync(...);7.3 可等待模式(Awaitable Pattern)
这是支持 await 操作的最低要求,不强制要求方法返回 Task。任何满足 GetAwaiter 要求的类型都可以被 await。
8. 编译器如何处理 await
当编译器遇到 await 表达式时,会进行以下转换:
- 调用 GetAwaiter() 获取等待器
- 检查 IsCompleted:
- 如果为 true,直接调用 GetResult()
- 如果为 false,注册 continuation 到 OnCompleted
- 生成状态机管理异步执行流
理解这一点有助于调试复杂的异步代码。
9. 跨平台注意事项
在不同平台上实现可等待类型时要注意:
- 线程模型差异:UI 线程在不同平台上的行为不同
- 同步上下文:ASP.NET Core 没有同步上下文
- 异常处理:某些平台对异常传播有特殊要求
- 性能特性:不同平台的异步原语性能特征不同
10. 未来发展方向
C# 异步编程仍在演进,一些值得关注的趋势:
- 异步流(Async Streams):IAsyncEnumerable 支持
- 异步释放:IAsyncDisposable 接口
- 更轻量的异步原语:如 IValueTaskSource
- 模式匹配增强:可能影响 await 表达式的处理
理解可等待模式的底层机制,可以帮助开发者更好地适应这些新特性。