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,并确认这些等待几乎从不跨线程 —— 这时才值得投入精力。
