TaskCompletionSource 是桥接非 awaitable 异步模式到 async/await 的桥梁,而非手动完成 Task 的工具;直接 new 后 SetResult 会导致未观察完成的 Task,引发 UnobservedTaskException 并可能终止进程。

TaskCompletionSource
为什么不能直接 new TaskCompletionSource().Task 后 SetResult?
这会导致 Task 处于 “completed but not observed” 状态,一旦后续代码抛出异常且未被 await 或 .Wait() 捕获,就会触发 UnobservedTaskException —— 在 .NET 6+ 默认会直接终止进程。
常见错误写法:
var tcs = new TaskCompletionSource<int>(); tcs.SetResult(42); // 危险!Task 已完成但无人 await return tcs.Task; // 调用方可能还没来得及 await 就已完成</int>
正确前提:必须确保 Task 的生命周期由使用者控制,SetResult/SetException 应发生在调用方已开始 await 之后(或至少能保证被观察)。
典型场景:把事件转成 async 方法(如 Button.Click)
这是最常用也最容易出错的用法。关键点不是“怎么转”,而是“谁负责取消、超时、重复触发”。
-
TaskCompletionSource默认不支持取消 —— 必须手动监听CancellationToken并调用tcs.TrySetCanceled() - 事件可能多次触发(比如用户狂点按钮),
TrySetResult()只有第一次生效,但后续事件仍会执行 —— 需显式注销事件处理器 - UI 线程回调需注意同步上下文:
TaskCompletionSource构造时传TaskCreationOptions.RunContinuationsAsynchronously可避免死锁(尤其在 WinForms/WPF)
示例(WPF):
public static Task<bool> WaitForClickAsync(this Button button, CancellationToken ct = default)
{
var tcs = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
RoutedEventHandler handler = null;
handler = (s, e) =>
{
if (tcs.TrySetResult(true))
button.Click -= handler; // 确保只响应一次
};
void CancelHandler(object _, EventArgs __) => tcs.TrySetCanceled(ct);
ct.Register(CancelHandler);
button.Click += handler;
return tcs.Task;
}</bool></bool>
SetException 和异常类型要严格匹配
TaskCompletionSource 接收 Exception 或 Exception[],但如果你传入 AggregateException,它会被包装成双层嵌套 —— 导致调用方 await 时抛出的异常是 AggregateException.InnerException,而不是你预期的那个原始异常。
- 直接传原始异常:
tcs.SetException(new InvalidOperationException("xxx")) - 不要传
new AggregateException(ex),除非你明确想让调用方看到外层包装 - 若需多异常(罕见),用
new Exception[] { ex1, ex2 },而非AggregateException
否则调用方 try { await MyMethod(); } catch (InvalidOperationException e) { ... } 会捕获不到。
别忽略 Completed 状态检查和 TrySetXXX 的返回值
SetResult/SetException/SetCanceled 是“强制设置”,一旦 Task 已完成再调就会抛 InvalidOperationException;而 TrySetXXX 返回 bool 表示是否成功设置 —— 这是线程安全的关键。
- 所有跨线程、事件驱动、回调场景,一律用
TrySetResult等,别假设“它一定没完成” - 返回
false意味着 Task 已被其他路径完成(比如超时、取消),此时你不该再做任何副作用(如释放资源、注销事件)—— 那些逻辑应放在完成后的 continuation 里,或用Task.ContinueWith(..., TaskContinuationOptions.OnlyOnRanToCompletion) - 不要在
TrySetXXX后继续执行业务逻辑,除非你确认它返回了true
真正的难点从来不在“怎么让 Task 完成”,而在于:谁拥有这个 Task 的所有权?完成时机是否与其他并发路径竞争?异常语义是否可预测?这些才是高级用法里藏坑最深的地方。











