TaskCompletionSource 是用于手动控制 Task 完成状态的工具,不涉及线程调度;它通过 TrySetResult/TrySetException/TrySetCanceled 方法响应外部信号,适用于将回调或事件转为 async/await 模式,并需谨慎处理同步回调、异常、取消和生命周期问题。

TaskCompletionSource 是什么,为什么不用直接 new Task
TaskCompletionSource 不是用来“创建运行中的任务”,而是用来“手动控制一个 Task 的完成状态”。它背后没有线程、不调度、不执行任何逻辑,只提供 SetResult、SetException、SetCanceled 这三个方法来终结其关联的 Task。直接 new Task 后调用 Start() 会触发线程调度,且无法从外部决定完成时机;而回调转 Task 的核心诉求恰恰是“等外部信号来了再结束”,所以必须用 TaskCompletionSource。
把事件或回调包装成 Task 的典型写法
比如你有一个老式 API:注册一个 Action 回调,操作完成后调用它;你想把它变成 async Task 方法:
public TaskDoWorkAsync() { var tcs = new TaskCompletionSource (); // 假设这是老接口:void LegacyApi(Actioncallback) LegacyApi(result => { tcs.TrySetResult(result); // 推荐用 TrySet* 系列,避免重复设置异常 }); return tcs.Task; }
TrySetResult比SetResult更安全:如果回调被意外触发多次,前者只生效第一次,后者抛InvalidOperationException- 务必处理异常路径:若
LegacyApi可能失败并传入Exception,对应调用tcs.TrySetException(ex)- 不要在回调里捕获异常后吞掉——这会让
await永远挂起常见陷阱:同步回调导致死锁或状态错乱
如果老接口是同步执行(比如立即调用回调),而你在 UI 线程或
async方法里调用它,TrySetResult会在当前线程立即触发Task完成,可能引发以下问题:
- 在 WinForms/WPF 中,若未配置
ConfigureAwait(false),后续await可能尝试切回 UI 线程,但此时线程正忙于执行回调,造成假死Task完成后立刻执行ContinueWith或await后续代码,若这些代码依赖某些尚未初始化的状态,会出错- 解决办法:强制异步化回调体,例如用
Task.Run(() => { ... })包一层,或在TrySet*前加await Task.Yield()(仅适用于 async 方法内部)取消支持:如何让 Task 可被 CancellationToken 触发
TaskCompletionSource本身不监听CancellationToken,需手动绑定:public TaskDoWorkAsync(CancellationToken ct) { var tcs = new TaskCompletionSource (TaskCreationOptions.RunContinuationsAsynchronously); using (ct.Register(() => tcs.TrySetCanceled())) { LegacyApi(result => tcs.TrySetResult(result)); // 注意:Register 返回的 IDisposable 必须保持引用到回调执行完, // 否则可能在回调前就被 GC,导致取消失效 } return tcs.Task;}
ct.Register返回的IDisposable必须存活到回调执行完毕,否则取消注册会提前失效- 更稳妥的做法是把
IDisposable存为局部变量,并确保它不会被提前释放(例如不要放进 using 块里就完事)- 如果
LegacyApi本身支持取消,优先用它的原生取消机制,而不是靠Register模拟真正难的不是调用
TrySetResult,而是判断回调到底在什么时机发生、是否可重入、是否可能失败、是否要响应取消——这些决定了TaskCompletionSource的生命周期管理方式。










