
TaskCompletionSource 不是启动任务,而是手动控制 Task 完成时机
很多人一看到 TaskCompletionSource<t></t> 就下意识当成 Task.Run 或 async 方法的替代品,这是最典型的误用起点。它不执行任何代码,也不调度线程;它只生成一个“悬而未决”的 Task<t></t>,然后等你调用 TrySetResult、TrySetException 或 TrySetCanceled 才真正结束。
这意味着:如果你在事件回调里忘了调用 TrySet*,那个 await 就永远卡住;如果你重复调用(比如事件触发两次),第二次会直接抛 InvalidOperationException —— 因为 Task 只能完成一次。
-
TrySet*比Set*更安全:避免多线程并发或重复触发导致异常 - 不要在构造后立刻
await tcs.Task,除非你确认有外部逻辑会触发完成 - 别把它和
CancellationTokenSource混用:TCS 本身不感知取消,需手动配合TrySetCanceled
把事件(Event)转成可 await 的 Task 最简模式
典型场景:第三方 SDK 只提供 Completed、KeyReceived 这类事件,但你的业务流程需要“等它发生再继续”。这时候 TCS 是桥接的唯一轻量选择。
关键不是“怎么写”,而是“怎么防崩”:
- 事件订阅必须在
TaskCompletionSource创建之后、await之前完成,否则可能丢事件 - 务必在事件处理函数里解绑(
-=),否则造成内存泄漏或重复调用 - 如果事件可能触发失败(比如网络断开没发出来),要加超时兜底,不能干等
示例片段(WPF 动画完成等待):
public static async Task<bool> AwaitStoryboardAsync(Storyboard sb)
{
var tcs = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
sb.Completed += (s, e) =>
{
sb.Completed -= (s2, e2) => { }; // 解绑
tcs.TrySetResult(true);
};
sb.Begin();
return await tcs.Task;
}</bool></bool>
TaskCreationOptions.RunContinuationsAsynchronously 很重要:它确保 await 后续代码不在 UI 线程同步执行,避免死锁(尤其在 WinForms/WPF 中)。
超时 + 多个事件竞争时用 Task.WhenAny 配合 TCS
真实系统里,事件不一定准时来。比如设备响应慢、网络抖动、回调丢失 —— 这时候光靠 TCS 不够,必须加超时保护。
常见错误是手写 Thread.Sleep 或轮询,正确做法是让延迟任务和结果任务“赛跑”:
-
Task.Delay是纯异步等待,不占线程 -
Task.WhenAny返回最先完成的那个Task,不是结果值 - 必须显式判断返回的
Task是哪个,再取对应结果(tcs.Task.Result或抛超时异常)
示例(等待 AVI 转码结果,15 秒超时):
var tcs = new TaskCompletionSource<bool>();
_transAviResultSource = tcs;
_transService.Trans2Avi(); // 触发外部流程
var timeout = Task.Delay(TimeSpan.FromSeconds(15));
var completed = await Task.WhenAny(tcs.Task, timeout);
if (completed == tcs.Task)
{
TransResult = await tcs.Task; // 安全:已完成
}
else
{
throw new TimeoutException("AVI 转码未在 15 秒内返回结果");
}</bool>
TCS 实例生命周期管理容易被忽略的点
很多人把 TaskCompletionSource 当成一次性局部变量,但在长生命周期对象(如 ViewModel、Service)中,它常被复用或缓存。这时几个细节决定稳定性:
- 不要复用已完成的 TCS 实例:必须 new 新的,否则
TrySet*失败 - 如果上层逻辑可能取消等待,TCS 本身不支持取消,你要在事件处理中检查
CancellationToken.IsCancellationRequested,并主动调用tcs.TrySetCanceled() - 在异常路径下(比如服务初始化失败),记得调用
tcs.TrySetException(),否则等待方永远挂起 - 调试时留意
Task.Status:如果是WaitingForActivation,说明还没被触发;RanToCompletion或Faulted才算落地
最隐蔽的问题是:TCS 没被 GC,因为它被闭包捕获、又被事件引用,最终变成内存泄漏。一旦发现某类操作越跑越慢,先查 TCS 是否解绑干净。











