c# IValueTaskSource 的 GetStatus 和 OnCompleted

10次阅读

GetStatus 总返回 Pending 是正常行为,因它仅在 GetResult 能立即安全返回时才返回 Succeeded 或 Faulted;错误提前返回 Succeeded 会导致读取未初始化值或 NULLReferenceException。

c# IValueTaskSource 的 GetStatus 和 OnCompleted

GetStatus 为什么总返回 ValueTaskSourceStatus.Pending

这是最常被误解的地方:GetStatus 不是“状态轮询接口”,而是供 ValueTask 内部判断是否可安全获取结果的契约方法。它必须严格反映底层状态:只有在 GetResult 已能立即返回(即已完成且未抛异常)时,才可返回 ValueTaskSourceStatus.Succeeded;若已失败,返回 ValueTaskSourceStatus.Faulted;否则一律返回 Pending

常见错误是提前返回 Succeeded —— 比如在异步操作刚启动、结果尚未写入 _result 字段时就改状态,会导致 ValueTask.GetAwaiter().GetResult() 读到未初始化值或引发 NullReferenceException

  • GetStatus 返回 Pending 是常态,不是 bug
  • 状态变更必须与 SetResult/SetException 的调用严格同步(通常需加锁或用 volatile.Write
  • 不要在 GetStatus 里做耗时检查(如轮询 IO 完成),它会被频繁调用

OnCompleted 的回调执行时机和线程约束

OnCompleted 接收一个 Action 和一个 Object 状态对象,它的唯一职责是:当操作**最终完成**(无论成功/失败)时,确保该 Action 被调用一次。它不承诺执行线程,也不保证立即执行 —— 典型实现是把回调压入 ThreadPool 或当前 SynchronizationContext

关键点在于“最终完成”:如果操作本身是同步完成的(比如缓存命中),OnCompleted 可能根本不会被调用(因为 ValueTask 的 awaiter 会直接走 GetResult 分支);如果异步完成,则必须确保回调只触发一次,且不能漏掉。

  • 务必用 Interlocked.CompareExchangevolatile 标记完成状态,防止重复调用 OnCompleted 的回调
  • 不要在 OnCompleted 内阻塞或做重逻辑,它可能运行在 IOCP 线程或 ui 线程上
  • 若需调度到特定上下文(如 winForms/wpf),应在回调内部手动 BeginInvoke,而非在 OnCompleted 里做

完整实现中容易漏掉的三个原子操作

手写 IValueTaskSource 时,90% 的崩溃来自状态竞争。以下三处必须原子化:

  • 标记“已完成”的标志位(如 _completed 字段)—— 必须用 volatileInterlocked
  • 写入结果字段(_result)—— 必须在标记完成前写入,且对读取端可见(Volatile.WriteMemoryBarrier
  • 保存异常引用(_exception)—— 同样需内存屏障,避免重排序导致 GetResult 读到 null 异常

下面是一个最小可行的无锁结构体实现片段(省略泛型封装):

struct ManualResetValueTaskSource : IValueTaskSource {     private T _result;     private Exception _exception;     private volatile int _state; // 0=Pending, 1=Succeeded, 2=Faulted      public ValueTaskSourceStatus GetStatus(short token) =>          _state switch         {             1 => ValueTaskSourceStatus.Succeeded,             2 => ValueTaskSourceStatus.Faulted,             _ => ValueTaskSourceStatus.Pending         };      public void OnCompleted(Action continuation, object state, short token, ValueTaskSourceOnCompletedFlags flags)     {         if (_state != 0) // 已完成,直接触发         {             ThreadPool.UnsafeQueueUserWorkItem(continuation, state);             return;         }          // 竞争设置为“正在完成”,仅一人成功         if (Interlocked.CompareExchange(ref _state, 1, 0) == 0)         {             // 设置结果后才允许 GetResult 读取             Volatile.Write(ref _result, default); // 占位,实际由 SetResult 填充             ThreadPool.UnsafeQueueUserWorkItem(continuation, state);         }     }      public T GetResult(short token) => _state switch     {         1 => _result,         2 => throw _exception!,         _ => throw new InvalidOperationException("Not completed")     };      public void SetResult(T result)     {         _result = result;         Volatile.Write(ref _state, 1);     }      public void SetException(Exception ex)     {         _exception = ex;         Volatile.Write(ref _state, 2);     } }

什么时候真该自己实现 IValueTaskSource

绝大多数场景不需要。.net 6+ 的 TaskCompletionSource 已足够高效;ValueTask 的核心价值在于避免分配,而自定义 IValueTaskSource 的收益只在高频、短生命周期、纯内存操作的场景下才明显(例如高性能网络库中的连接池等待、无锁队列的出队等待)。

如果你只是想“让方法返回 ValueTask”,直接用 async Task + ConfigureAwait(false) 更安全;若用了 Task.FromResult,考虑换成 ValueTask(value) 构造函数即可。

真正需要手写的信号很明确:你正在压测发现 TaskCompletionSource 的 GC 分配成了瓶颈,且 profiler 显示大量 Task 对象存活在 Gen0,并确认这些等待几乎从不跨线程 —— 这时才值得投入精力。

Copyright ©  SEO

 Theme by Puock