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

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.CompareExchange或volatile标记完成状态,防止重复调用OnCompleted的回调 - 不要在
OnCompleted内阻塞或做重逻辑,它可能运行在 IOCP 线程或 UI 线程上 - 若需调度到特定上下文(如 WinForms/WPF),应在回调内部手动
BeginInvoke,而非在OnCompleted里做
完整实现中容易漏掉的三个原子操作
手写 IValueTaskSource 时,90% 的崩溃来自状态竞争。以下三处必须原子化:
- 标记“已完成”的标志位(如
_completed字段)—— 必须用volatile或Interlocked - 写入结果字段(
_result)—— 必须在标记完成前写入,且对读取端可见(Volatile.Write或MemoryBarrier) - 保存异常引用(
_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
什么时候真该自己实现 IValueTaskSource?
绝大多数场景不需要。.NET 6+ 的 TaskCompletionSource 已足够高效;ValueTask 的核心价值在于避免分配,而自定义 IValueTaskSource 的收益只在高频、短生命周期、纯内存操作的场景下才明显(例如高性能网络库中的连接池等待、无锁队列的出队等待)。
如果你只是想“让方法返回 ValueTask”,直接用 async Task + ConfigureAwait(false) 更安全;若用了 Task.FromResult,考虑换成 ValueTask 构造函数即可。
真正需要手写的信号很明确:你正在压测发现 TaskCompletionSource 的 GC 分配成了瓶颈,且 profiler 显示大量 Task 对象存活在 Gen0,并确认这些等待几乎从不跨线程 —— 这时才值得投入精力。










