TaskCompletionSource的核心作用是手动创建可await的Task,通过SetResult/SetException/SetCanceled控制其完成状态,实现回调到async/await的轻量桥接。

TaskCompletionSource 的核心作用:手动捏造一个可 await 的 Task
它不执行任何异步逻辑,也不启动线程——它只是给你一个 Task 实例的“遥控器”。你用 TaskCompletionSource 创建出一个未完成的 Task,然后在任意时机(比如事件回调、第三方 SDK 通知、UI 线程响应后)调用 SetResult()、SetException() 或 SetCanceled(),来强行“推”这个 Task 进入终态。这是把“回调驱动”转成“async/await 驱动”的最轻量级桥梁。
怎么手动控制 Task:三步走,缺一不可
常见错误是只创建了 TaskCompletionSource 却忘了暴露它的 Task,或者在多线程环境下没注意线程安全调用 Set* 方法(虽然 Set* 本身是线程安全的,但业务逻辑可能不是)。
- 第一步:创建实例,声明你要返回的类型,比如
new TaskCompletionSource或() new TaskCompletionSource() - 第二步:把
tcs.Task返回出去,供调用方await;别直接 awaittcs——它不是 Task - 第三步:在真正该结束的时候(例如按钮点击、WebSocket 收到响应、Timer 触发),调用对应方法:
• 成功 →tcs.SetResult("done")
• 失败 →tcs.SetException(new InvalidOperationException("timeout"))
• 取消 →tcs.SetCanceled()(注意:这会触发OperationCanceledException)
典型场景:包装 UI 弹窗、事件、老式 Begin/End 模式
比如你在 WPF 或 MAUI 中弹登录框,不能直接 await ShowDialog()(它同步阻塞)。这时就用 TaskCompletionSource 桥接:
private TaskCompletionSource_loginTcs; public async Task ShowLoginAsync() { _loginTcs = new TaskCompletionSource (); var window = new LoginWindow(); window.LoginCompleted += (result) => _loginTcs.SetResult(result); // 事件回调里推进 window.Show(); return await _loginTcs.Task; }
⚠️ 注意:如果用户关掉窗口没触发事件,_loginTcs.Task 就永远挂起——必须配超时或取消逻辑,否则会内存泄漏+死等。
容易踩的坑:重复 Set / 忘记 Set / 线程错乱
TaskCompletionSource 是一次性状态机:一旦调用了 SetResult,再调一次就会抛 InvalidOperationException: "The task has already been completed."。这不是 bug,是设计使然。
- 别在多个地方无保护地调用
Set*——加Interlocked.CompareExchange或用if (tcs.TrySetResult(...))更安全(TrySet*系列方法会静默失败,适合竞态场景) - 别漏掉异常路径:比如网络请求超时、事件没订阅成功、回调被 GC 掉,都可能导致 Task 永远不完成
- 不要在非 UI 线程直接操作 WPF/WinForms 控件后再调
SetResult——先Dispatcher.Invoke或BeginInvoke回 UI 线程,再 Set
最常被忽略的一点:它和 CancellationToken 没有自动绑定。你想支持取消,得自己监听 token 并在 token.Register(() => tcs.TrySetCanceled()),而不是指望 TaskCompletionSource 自动感知。
