taskcompletionsource 用于手动控制 task 完成,适用于响应外部异步信号;它不执行代码、不创建线程,需配合事件订阅、取消注册和 runcontinuationsasynchronously 选项确保 ui 安全与正确性。

TaskCompletionSourceTask<t></t>——它本身不跑代码、不开线程、不调度,只等你调用 TrySetResult、TrySetException 或 TrySetCanceled。
为什么不能直接 new Task().Start()?
因为 new Task<t>(...).Start()</t> 会立即把委托扔进线程池执行,完成时机由委托自身决定;而你要桥接的是“外部信号”:比如事件触发、回调到达、I/O 完成通知。这些信号不受你控制,也不在你当前线程里发生。用 TaskCompletionSource 才能真正实现“等它来,再结束”。
-
Task.Run和async方法生成的Task是“自动完成”的,适合封装同步逻辑或已知 I/O 路径 -
TaskCompletionSource生成的是“悬而未决”的Task,适合封装未知何时发生的响应式信号 - 直接
new Task(...)后Start(),一旦委托抛异常且没被await捕获,就会变成未观察异常(unobserved exception),.NET 可能直接终止进程
事件转 await:订阅顺序和解绑必须严格对齐
WPF 或 WinForms 中等待 Storyboard.Completed、Button.Click 这类事件时,最常见错误是:先 await tcs.Task,再订阅事件——结果事件在 await 前就触发了,tcs 永远不会完成。
- 必须先创建
TaskCompletionSource,再订阅事件,再触发动作(如sb.Begin()) - 事件处理函数里要用
-=解绑,否则重复点击/多次动画会触发多次TrySet*,第二次调用TrySetResult会静默失败(TrySet*返回false),但若误用SetResult则直接抛InvalidOperationException - 推荐写法:用局部变量捕获 handler,确保解绑目标明确
var tcs = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
RoutedEventHandler handler = null;
handler = (s, e) =>
{
if (tcs.TrySetResult(true))
button.Click -= handler;
};
button.Click += handler;
</bool>
取消支持必须手动绑定 CancellationToken
TaskCompletionSource 本身对 CancellationToken 完全无感。你不显式监听,它就不会响应取消请求。
- 别指望
await tcs.Task.WithCancellation(ct)自动生效——那是扩展方法的糖衣,底层仍需你配合ct.Register(() => tcs.TrySetCanceled()) - 注册取消回调时,要确保
Register在事件订阅之前完成,否则取消可能漏掉 - 如果事件回调和取消回调可能并发触发,
TrySetCanceled和TrySetResult天然互斥(只会成功一次),无需额外锁
RunContinuationsAsynchronously 是 UI 线程安全的关键选项
在 WPF/WinForms 中,如果省略 TaskCreationOptions.RunContinuationsAsynchronously,await 后续代码可能在 UI 线程同步执行,导致死锁(尤其当你在 Dispatcher.Invoke 或同步上下文里 await 一个还没完成的 tcs.Task)。
- 这个选项强制后续
await继续体走线程池,避免卡住 UI 线程 - 它不影响
TrySet*的调用线程——你仍可在任意线程调用它 - 不加它不是“一定出错”,但在复杂 UI 流程中极易触发难以复现的挂起
最危险的坑不在语法,而在生命周期:事件可能永不触发、回调可能被吞、取消注册可能遗漏、UI 线程同步续体可能卡死。所有这些,都不会报编译错误,但会让 await 永远停在那里——没有超时、没有日志、没有堆栈,只有静默等待。











