TaskCompletionSource 是什么,为什么不用直接 new Task
TaskCompletionSource不是用来“创建运行中的任务”,而是用来“手动控制一个
Task的完成状态”。它背后没有线程、不调度、不执行任何逻辑,只提供
SetResult、
SetException、
SetCanceled这三个方法来终结其关联的
Task。直接 new
Task后调用
Start()会触发线程调度,且无法从外部决定完成时机;而回调转
Task的核心诉求恰恰是“等外部信号来了再结束”,所以必须用
TaskCompletionSource。
把事件或回调包装成 Task 的典型写法
比如你有一个老式 API:注册一个
Action<string></string>回调,操作完成后调用它;你想把它变成
async Task<string></string>方法:
public Task<string> DoWorkAsync()
{
var tcs = new TaskCompletionSource<string>();
<pre class="brush:php;toolbar:false;">// 假设这是老接口:void LegacyApi(Action<string> callback)
LegacyApi(result =>
{
tcs.TrySetResult(result); // 推荐用 TrySet* 系列,避免重复设置异常
});
return tcs.Task;}
TrySetResult比
SetResult更安全:如果回调被意外触发多次,前者只生效第一次,后者抛
InvalidOperationException务必处理异常路径:若
LegacyApi可能失败并传入
Exception,对应调用
tcs.TrySetException(ex)不要在回调里捕获异常后吞掉——这会让
await永远挂起
常见陷阱:同步回调导致死锁或状态错乱
如果老接口是同步执行(比如立即调用回调),而你在 UI 线程或
async方法里调用它,
TrySetResult会在当前线程立即触发
Task完成,可能引发以下问题: 在 WinForms/WPF 中,若未配置
ConfigureAwait(false),后续
await可能尝试切回 UI 线程,但此时线程正忙于执行回调,造成假死
Task完成后立刻执行
ContinueWith或
await后续代码,若这些代码依赖某些尚未初始化的状态,会出错 解决办法:强制异步化回调体,例如用
Task.Run(() => { ... }) 包一层,或在 TrySet*前加
await Task.Yield()(仅适用于 async 方法内部)
取消支持:如何让 Task 可被 CancellationToken 触发
TaskCompletionSource本身不监听
CancellationToken,需手动绑定:
public Task<string> DoWorkAsync(CancellationToken ct)
{
var tcs = new TaskCompletionSource<string>(TaskCreationOptions.RunContinuationsAsynchronously);
<pre class="brush:php;toolbar:false;">using (ct.Register(() => tcs.TrySetCanceled()))
{
LegacyApi(result => tcs.TrySetResult(result));
// 注意:Register 返回的 IDisposable 必须保持引用到回调执行完,
// 否则可能在回调前就被 GC,导致取消失效
}
return tcs.Task;}
ct.Register返回的
IDisposable必须存活到回调执行完毕,否则取消注册会提前失效 更稳妥的做法是把
IDisposable存为局部变量,并确保它不会被提前释放(例如不要放进 using 块里就完事) 如果
LegacyApi本身支持取消,优先用它的原生取消机制,而不是靠
Register模拟
真正难的不是调用
TrySetResult,而是判断回调到底在什么时机发生、是否可重入、是否可能失败、是否要响应取消——这些决定了
TaskCompletionSource的生命周期管理方式。
