c# IValueTaskSource 的 GetStatus 和 OnCompleted

来源:这里教程网 时间:2026-02-21 17:40:13 作者:

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

关键点在于“最终完成”:如果操作本身是同步完成的(比如缓存命中),

OnCompleted
可能根本不会被调用(因为
ValueTask
的 awaiter 会直接走
GetResult
分支);如果异步完成,则必须确保回调只触发一次,且不能漏掉。

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

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

手写

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

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

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

struct ManualResetValueTaskSource<T> : IValueTaskSource<T>
{
    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<object> 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<t></t>

绝大多数场景不需要。.NET 6+ 的

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

如果你只是想“让方法返回

ValueTask
”,直接用
async Task<t></t>
+
ConfigureAwait(false)
更安全;若用了
Task.FromResult
,考虑换成
ValueTask<t>(value)</t>
构造函数即可。

真正需要手写的信号很明确:你正在压测发现

TaskCompletionSource<t></t>
的 GC 分配成了瓶颈,且 profiler 显示大量
Task
对象存活在 Gen0,并确认这些等待几乎从不跨线程 —— 这时才值得投入精力。

相关推荐