c# TaskCompletionSource 和 ManualResetEventSlim 的异同

TaskCompletionSource是手动控制的异步结果容器,用于将同步/回调逻辑转为可await的Task;ManualResetEventSli

m是轻量级线程同步原语,用于多线程间信号通知,二者解决不同维度问题,不可互换。

TaskCompletionSource 适合封装异步操作结果

当你需要把一个同步或回调式逻辑“包装”成 Task,供 await 消费时,TaskCompletionSource 是首选。它不阻塞线程,只负责传递完成状态和结果,本质是“可手动控制的 Task 工厂”。

常见场景包括:模拟延迟、桥接事件(如 UI 控件点击)、封装第三方回调 API、实现自定义 awaitable。

  • SetResult()SetException()SetCanceled() 必须且只能调用一次,重复调用抛 InvalidOperationException
  • 构造时不传参数,默认使用 TaskCreationOptions.None;若需取消传播,可传 TaskCreationOptions.RunContinuationsAsynchronously
  • Task 属性是冷任务(cold task),不会自动执行,也不绑定线程
var tcs = new TaskCompletionSource();
// 在某个回调里:
tcs.SetResult(42);
// 后续可 await tcs.Task;

ManualResetEventSlim 用于线程间信号通知

ManualResetEventSlim 是轻量级同步原语,核心用途是让一个或多个线程等待某个条件成立(例如资源就绪、操作完成),然后被唤醒。它本质是“手动置位 + 多线程等待”的信号量,和 Task 体系无关。

典型场景:生产者-消费者模式中的空/满信号、等待后台初始化完成、协调多线程启动时机。

  • 支持自旋 + 内核切换双阶段,默认自旋 10 次,适合短等待;可通过构造函数调整 spinCount
  • Set() 置位后,所有等待线程立即唤醒,且保持置位状态直到显式调用 Reset()
  • 没有泛型参数,不携带数据;若需传递结果,得配合其他变量(注意加锁或用 Volatile.Read/Write
var mres = new ManualResetEventSlim(false);
// 线程 A:
Task.Run(() => {
    Thread.Sleep(1000);
    mres.Set(); // 通知完成
});
// 线程 B:
mres.Wait(); // 阻塞直到 Set()

别把两者当替代品用

它们解决的是不同维度的问题:TaskCompletionSource 是异步编程模型的“结果容器”,面向 async/await 流;ManualResetEventSlim 是同步协调工具,面向线程阻塞与唤醒。强行混用容易引入死锁或性能陷阱。

  • async 方法里调用 mres.Wait() 会阻塞当前线程,可能拖垮线程池;应改用 mres.WaitAsync()(.NET 6+)或包装成 Task 手动调度
  • TaskCompletionSource 实现“等待信号”功能,虽可行但冗余——它不提供等待能力,还得额外配 Task.Delay 或轮询,不如直接用 WaitHandleChannel
  • ManualResetEventSlim 无法参与 await usingValueTask 优化;而 TaskCompletionSourceTask 可以被 ValueTask 封装复用(需谨慎)

容易忽略的细节

真正出问题的地方往往藏在边界行为里:

  • TaskCompletionSource 构造时若传入 TaskCreationOptions.RunContinuationsAsynchronously,后续 await 的 continuation 一定在 ThreadPool 线程执行;否则可能在调用 SetXXX 的同一线程同步执行(影响 UI 响应或造成栈溢出)
  • ManualResetEventSlimWait() 在 .NET Core/.NET 5+ 中支持取消令牌,但 CancellationToken 触发时抛 OperationCanceledException,不是静默返回;而 WaitAsync() 返回 Task,更符合 async 场景
  • 两者都不自带超时逻辑:TaskCompletionSource 要靠外部 Task.Delay().ContinueWith()TimeoutAfter() 扩展;ManualResetEventSlim.Wait()WaitAsync() 都有带 timeout 的重载,但单位是毫秒,不是 TimeSpan(易写错)