C#中TaskCompletionSource(简称 TCS)学习
今天看到一个 TaskCompletionSource类,感觉超级有用,解决了我一个很困惑的问题。
- 概念(高层)
- TaskCompletionSource<TResult> 是一个“手动完成”的 Task 生产器。它创建并持有一个 Task(通过 .Task 暴露),程序可以在未来的任意时间通过 TrySetResult/TrySetException/TrySetCanceled 来完成该 Task(成功、失败或取消)。
- 用途:把基于回调/事件/外部信号的异步结果桥接到 async/await(即生产者决定何时完成,而不是由 .NET 运行时自动完成)。
- 基本 API(常用)
- 构造:new TaskCompletionSource<TResult>(); 或 new TaskCompletionSource<TResult>(TaskCreationOptions.RunContinuationsAsynchronously);
- 读取任务:var task = tcs.Task;
- 完成成功:tcs.TrySetResult(value) 或 tcs.SetResult(value)
- 标记异常:tcs.TrySetException(exception) 或 tcs.SetException(exception)
- 标记取消:tcs.TrySetCanceled() / TrySetCanceled(cancellationToken)
- 注意:TrySetXxx 更稳健(不会抛异常),SetXxx 在重复完成时会抛 InvalidOperationException。
- 最简单示例
var tcs = new TaskCompletionSource<int>(); Task<int> t = tcs.Task; // 在某个异步操作完成时: tcs.TrySetResult(42); // 调用方: int value = await t; // value == 42
- 转换回调到 await(经典用例)
把事件/回调转换为 Task:
Task<string> WaitForEventAsync() { var tcs = new TaskCompletionSource<string>(TaskCreationOptions.RunContinuationsAsynchronously); SomeEventSource.OnCompleted += Handler; void Handler(object s, string result) { SomeEventSource.OnCompleted -= Handler; tcs.TrySetResult(result); } return tcs.Task; }
- 异常与取消传播
- 在消费端(生产者完成 TCS 的代码)应在异常路径调用 TrySetException(ex),在取消路径调用 TrySetCanceled(),否则等待方会一直挂起。
- 生产者和等待方可以约定用哪种异常或取消方案来表示具体错误/超时。
- RunContinuationsAsynchronously 参数(重要)
- 默认情况下,Task 的 await 续体可能在完成 Task 的线程上同步执行(即当你调用 TrySetResult 时,await 的续体有时会立即执行在当前线程)。这在某些情形会导致死锁或阻塞(尤其与同步上下文/UI 线程混用时)。
- 建议创建时传入 TaskCreationOptions.RunContinuationsAsynchronously,强制把续体异步调度,从而避免在完成方线程同步执行续体的风险:
var tcs = new TaskCompletionSource<TResult>(TaskCreationOptions.RunContinuationsAsynchronously);
- 取消与超时示例(带 CancellationToken)
var tcs = new TaskCompletionSource<string>(TaskCreationOptions.RunContinuationsAsynchronously); using (cancellationToken.Register(() => tcs.TrySetCanceled(cancellationToken))) { // 启动等待的工作/入队等 return await tcs.Task; // 若 cancellationToken 被触发,则会抛 TaskCanceledException }
注意:Register 返回的 IDisposable 要 Dispose(通常用 using 或在 finally 中)。
- 超时等待模式(调用方)
var tcs = new TaskCompletionSource<int>(TaskCreationOptions.RunContinuationsAsynchronously); // ...触发工作 var completed = await Task.WhenAny(tcs.Task, Task.Delay(TimeSpan.FromSeconds(10))); if (completed != tcs.Task) throw new TimeoutException(); int result = await tcs.Task; // 若 Task 被异常完成,这里会抛
- 线程安全与行为
- TaskCompletionSource 的 TrySetXxx/SetXxx 方法都是线程安全的。只允许“单次完成”——首次成功的 TrySet*** 将生效,后续尝试返回 false(TrySet)或抛异常(Set)。
- 因为只能完成一次,通常一条工作链只创建一个 TCS。
- 常见陷阱
- 不用 RunContinuationsAsynchronously 可能导致续体在 Set 的线程上执行,引起性能或死锁(特别是 UI/同步上下文场景)。
- 忘记在异常路径设置 TrySetException,会导致等待方永远挂起(内存/资源泄漏)。
- 忘记处理或 Dispose CancellationToken 注册,会造成内存泄漏。
- 将 TCS 误用为“多次完成”的信号(TCS 只能完成一次)。
- 同步等待 Task.Result/Wait() (特别在 UI/ASP.NET 同步上下文)容易造成死锁;尽量使用 await。
- 与其它同步原语对比
- ManualResetEventSlim / TaskCompletionSource:MRE 是阻塞等待(同步),而 TCS 是异步/可 await 的。
- Task.Run:用于把工作放在线程池执行,不用于“等待某外部事件由其他线程完成”的桥接。TCS 用于表示未来由外部决定完成的单次结果。
- Channels / IAsyncEnumerable:用于持续的多项异步流,若需要多次、流式结果,不应用单个 TCS。
- 使用场景建议
- 将基于回调/事件的 API 包装为 async/await。
- 在生产者-消费者场景中,为每个入队项提供一个 TCS,让调用方能够“入队并等待”该项被处理。
- 在测试、RPC、等待一次性信号或异步完成通知时使用。
- 调试技巧
- 如果 await 某个 TCS.Task 永远不返回,检查消费端是否在所有路径(成功/异常/取消)都设置了 TrySetXxx。
- 可以把 TrySetException 的异常做日志记录以便追踪失败原因。
- 使用 TaskCreationOptions.RunContinuationsAsynchronously 后,续体的堆栈信息更容易与异步上下文分离,调试更清晰。
- 示例:用 TCS 实现一次性事件等待(独立)
class OneShotSignal { private TaskCompletionSource<bool> _tcs = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously); public Task WaitAsync() => _tcs.Task; public void Signal() => _tcs.TrySetResult(true); public void Fail(Exception ex) => _tcs.TrySetException(ex); public void Cancel() => _tcs.TrySetCanceled(); }
总结(要点)
- TaskCompletionSource 非常强大:能把任意“未来会发生”的事件表示为 Task,方便 await。
- 永远优先使用 TrySetXXX,并在创建时考虑 TaskCreationOptions.RunContinuationsAsynchronously。
- 关注取消、异常处理以及注册的释放,避免资源泄漏或死等。
- 当需要表示多次或流式结果时,使用 Channel / IAsyncEnumerable 等更合适的数据结构。
浙公网安备 33010602011771号