<p>TaskCompletionSource 不用于启动异步任务,仅生成可手动完成的 Task;await tcs.Task 卡住因 Task 默认处于 WaitingForActivation 状态且无人调用 TrySet 方法;必须用 TrySet 防重复设置与异常,配合事件解绑、超时保护(Task.WhenAny)及 RunContinuationsAsynchronously 避免 UI 死锁,CancellationToken 需手动监听。</p>

TaskCompletionSourceTask<t></t>;用错场景(比如替代 Task.Run 或 async 方法)是绝大多数崩溃和死锁的起点。
为什么 await tcs.Task 会卡住不动?
因为 TaskCompletionSource<t></t> 创建出来的 Task 默认处于 WaitingForActivation 状态,它不会自动完成——没人调用 TrySetResult、TrySetException 或 TrySetCanceled,它就永远挂在那里。
- 常见错误:在事件订阅前就
await tcs.Task,结果事件先触发、后订阅,导致回调永远不执行,Task永不完成 - 更隐蔽的问题:事件触发逻辑里漏了
tcs.TrySet*,或写成了Set*(重复调用直接抛InvalidOperationException) - UI 场景下没加
TaskCreationOptions.RunContinuationsAsynchronously,续体在 UI 线程同步执行,可能引发死锁
该用 TrySet* 还是 Set*?
TrySet* 是生产环境唯一安全的选择。它返回 bool:成功返回 true,失败(比如已设置过、已被取消)返回 false,不抛异常;而 Set* 在非法状态下会直接炸出 InvalidOperationException。
- 事件可能被多次触发(比如按钮双击、网络重试),
TrySetResult可防重复设置 - 多线程环境下(如后台轮询 + 用户主动取消),
TrySetCanceled比SetCanceled更健壮 - 务必配合解绑:在回调里调用
tcs.TrySet*后,立刻移除事件订阅(-=),否则内存泄漏+重复触发
如何给 TCS 加超时保护?
不能靠 Thread.Sleep 或轮询,正确做法是用 Task.WhenAny 让超时任务和结果任务“赛跑”。
-
Task.Delay(timeout, ct)是纯异步等待,不占线程 -
Task.WhenAny(tcs.Task, Task.Delay(...))返回最先完成的那个Task,但不等于结果值 - 必须显式判断:
if (completed == tcs.Task) return await tcs.Task;,否则直接await可能拿到Task.Delay的完成信号 - 超时后记得调用
tcs.TrySetCanceled()(如果还没完成),避免资源滞留
TaskCreationOptions.RunContinuationsAsynchronously 为什么关键?
它决定 await 后续代码(续体)是否强制切到线程池执行,而不是回到原始上下文(如 WinForms/WPF 的 UI 线程)同步运行。
- 不加这个选项,在 UI 线程中
await tcs.Task后,续体会试图在 UI 线程同步执行——若此时 UI 线程正忙(比如在处理消息循环),就会卡死 - 加了之后,续体交由线程池调度,彻底规避 UI 死锁风险
- ASP.NET Core 默认无同步上下文,影响较小;但 WinForms/WPF/旧版 ASP.NET 必须加
最易被忽略的一点:TCS 本身不感知取消,CancellationToken 需要你手动监听并调用 TrySetCanceled;很多人以为传个 cancellationToken 参数进去就自动生效,其实完全不是一回事。


















