TaskCompletionSource 的核心作用:手动捏造一个可 await 的 Task
它不执行任何异步逻辑,也不启动线程——它只是给你一个
Task实例的“遥控器”。你用
TaskCompletionSource<t></t>创建出一个未完成的
Task<t></t>,然后在任意时机(比如事件回调、第三方 SDK 通知、UI 线程响应后)调用
SetResult()、
SetException()或
SetCanceled(),来强行“推”这个 Task 进入终态。这是把“回调驱动”转成“async/await 驱动”的最轻量级桥梁。
怎么手动控制 Task:三步走,缺一不可
常见错误是只创建了
TaskCompletionSource却忘了暴露它的
Task,或者在多线程环境下没注意线程安全调用
Set*方法(虽然
Set*本身是线程安全的,但业务逻辑可能不是)。 第一步:创建实例,声明你要返回的类型,比如
new TaskCompletionSource<bool>()</bool>或
new TaskCompletionSource<string>()</string>第二步:把
tcs.Task返回出去,供调用方
await;别直接 await
tcs——它不是 Task 第三步:在真正该结束的时候(例如按钮点击、WebSocket 收到响应、Timer 触发),调用对应方法:
• 成功 →
tcs.SetResult("done")• 失败 →
tcs.SetException(new InvalidOperationException("timeout"))• 取消 →
tcs.SetCanceled()(注意:这会触发
OperationCanceledException)
典型场景:包装 UI 弹窗、事件、老式 Begin/End 模式
比如你在 WPF 或 MAUI 中弹登录框,不能直接
await ShowDialog()(它同步阻塞)。这时就用
TaskCompletionSource桥接:
private TaskCompletionSource<LoginResult> _loginTcs;
public async Task<LoginResult> ShowLoginAsync()
{
_loginTcs = new TaskCompletionSource<LoginResult>();
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自动感知。
