ValueTask 应该怎么 await
前言
平时拿到 ValueTask,直接 await 看起来没有什么可讨论的。但在 .NET runtime 和 ASP.NET Core 的热点代码里,经常能看到另一种写法:先检查 IsCompletedSuccessfully,同步成功时直接读取结果,只有未完成时才等待。
这很容易让人产生疑问:调用方是不是也应该先判断完成状态?直接 await 会不会错过优化?又为什么有些代码会把 ValueTask 转成 Task?本文就围绕这些问题展开,重点区分普通调用方的写法和框架内部的优化写法。
先说结论:拿到一个 ValueTask 后,绝大多数情况下直接 await 就可以了。
var result = await valueTask;
前面提到的框架写法会先处理同步成功的情况;如果操作尚未完成,或者已经失败、取消,再交给后面的异步分支。
这不是让每个调用方都在 await 前做判断,而是框架在性能热点上使用的一种优化。后文会结合 ASP.NET Core v10.0.0 的源码来看它具体是怎么写的。
直接 await 就会检查完成状态
await 并不是无条件挂起。编译器会先取得 awaiter,再检查它的 IsCompleted:
var awaiter = valueTask.GetAwaiter();
if (!awaiter.IsCompleted)
{
// 保存 async 方法的状态,并注册完成后的后续回调。
// 恢复后会调用 awaiter.GetResult()。
}
else
{
awaiter.GetResult();
}
上面的代码只是为了说明流程,实际生成的代码还会处理状态机字段、异常和上下文。关键逻辑并不复杂:
- 已成功完成:直接取结果,不注册后续回调。
- 已失败或已取消:同样直接调用
GetResult(),让异常或取消按await的语义抛出。 - 尚未完成:注册后续回调,完成后再调用
GetResult()。
也就是说,调用方自己判断 IsCompleted 并不能省掉这次检查,反而需要自己处理成功、失败和取消三种已完成状态。普通代码直接交给 await 即可。
为什么同一个 ValueTask 不能多次 await
Task 是可共享的对象。多个调用方可以等待同一个 Task,任务完成后,结果也会一直保留。
ValueTask 没有这样的约定。它只是一个结构体,内部可能表示下面三种情况:
- 已经得到的同步结果。例如
ValueTask.FromResult(42)直接把42放在ValueTask<int>中。 - 一个正在执行或已经完成的
Task/Task<T>。这时ValueTask只是对原Task的包装,例如new ValueTask<int>(someTask)。 - 一个
IValueTaskSource,以及标识本次操作的版本令牌。这个对象可以被复用,用来避免每次操作都创建Task。
注意:这里说的是
ValueTask的内部表示,不是方法表面上是否写了async。一个async ValueTask<T>方法也完全可能同步完成,例如缓存命中后直接返回结果;但编译器生成的 builder 如何构造返回值是实现细节,调用方不应据此判断ValueTask的来源。
前两种情况在很多实现中确实可以多次等待,但调用方无法从方法签名判断实际拿到的是哪一种。只要 API 返回的是 ValueTask,就必须按第三种情况的约束来使用。
真正的限制来自 ValueTask 包装 IValueTaskSource 的情况。高吞吐 I/O 会把 IValueTaskSource 放进对象池中复用:一次操作完成并被消费后,同一个 IValueTaskSource 就可以开始下一次操作。ValueTask 保存的版本令牌用来标识“这是第几次操作”。
第 1 次操作:IValueTaskSource + token 17 -> ValueTask A
await A:消费结果
IValueTaskSource 被归还并开始第 2 次操作,token 变为 18
再次 await A:A 仍携带 token 17,已不再代表当前操作
如果在 IValueTaskSource 被复用后再次等待 A,通常会因为令牌不匹配而失败。即使它还没被复用,也未必支持登记多个后续回调。具体表现取决于实现,不能依赖“第二次能正常工作”,也不能假设一定会抛出某一种异常。
这也解释了为什么有些 ValueTask 看上去可以多次 await:它们刚好包装了 Task,或者自身保存了同步结果。调用方无法从方法签名判断 ValueTask 的来源,因此应当遵守最严格的约束:一个 ValueTask 实例只能消费一次。
var valueTask = reader.ReadAsync(cancellationToken);
// 正确:只消费一次。
var result = await valueTask;
// 不要再次 await valueTask,也不要再次调用 valueTask.AsTask()。
若确实需要多个等待者、缓存结果或传给 Task.WhenAll,转换一次并保存该 Task:
Task<ReadResult> task = reader.ReadAsync(cancellationToken).AsTask();
await Task.WhenAll(task, RecordCompletionAsync(task));
var result = await task;
这样做可能会多分配一个 Task,但换来了 Task 支持多次等待和多个观察者的语义。官方文档将多次 await、重复调用 AsTask(),以及混合两种消费方式都列为不受支持的用法。Stephen Toub 的 Understanding the Whys, Whats, and Whens of ValueTask 也介绍了可复用 IValueTaskSource 用来避免分配的背景。更多约束可以参考 ValueTask API 文档。
一个实际会报错的例子
下面的示例使用 ManualResetValueTaskSourceCore<T> 模拟可复用操作。第一次等待完成后,代码立刻复用同一个 IValueTaskSource 创建第二次操作,然后再次等待旧的 ValueTask。
using System.Threading.Tasks.Sources;
var source = new ReusableOperation();
var first = source.CreateCompleted(1);
Console.WriteLine($"First result: {await first}");
// source 开始下一次操作,版本令牌发生变化。
_ = source.CreateCompleted(2);
try
{
Console.WriteLine($"Second result: {await first}");
}
catch (Exception exception)
{
Console.WriteLine($"{exception.GetType().FullName}: {exception.Message}");
}
public sealed class ReusableOperation : IValueTaskSource<int>
{
private ManualResetValueTaskSourceCore<int> _core;
public ValueTask<int> CreateCompleted(int result)
{
_core.Reset();
_core.SetResult(result);
return new ValueTask<int>(this, _core.Version);
}
int IValueTaskSource<int>.GetResult(short token) => _core.GetResult(token);
ValueTaskSourceStatus IValueTaskSource<int>.GetStatus(short token) =>
_core.GetStatus(token);
void IValueTaskSource<int>.OnCompleted(
Action<object?> continuation,
object? state,
short token,
ValueTaskSourceOnCompletedFlags flags) =>
_core.OnCompleted(continuation, state, token, flags);
}
在本机的 .NET 10.0.0 上运行,输出为:
First result: 1
System.InvalidOperationException: Operation is not valid due to the current state of the object.
异常由 ManualResetValueTaskSourceCore<T> 在发现旧版本令牌时抛出。本例的异常类型和消息只是这个实现的具体行为,不能把它当成 ValueTask 多次 await 时的通用结果;不同的 IValueTaskSource 可以表现不同。这也是调用方不能依赖第二次 await 的原因。
为什么源码里使用 IsCompletedSuccessfully
IsCompleted 表示操作已经不在 Pending 状态,其中包括成功、失败和取消。IsCompletedSuccessfully 则进一步保证操作已经成功完成,因此这时读取 Result 是安全的。
这类判断适合放在非 async 方法的快速路径上:成功时同步处理结果并立即返回;未完成、失败和取消则交给 await,由它处理后续回调、异常和取消。
调用底层操作
-> IsCompletedSuccessfully ?
是:直接读取 Result,继续同步执行
否:调用局部异步方法,由 await 等待结果
外层方法通常不会标记为 async。否则编译器仍然需要为它生成状态机,快速路径的意义就小很多。下面直接看 ASP.NET Core .NET 10 中几种实际写法。
为什么有些源码会调用 AsTask
这种写法通常出现在方法必须返回 Task 的地方。这里调用 AsTask() 不是为了让 await 更好用,而是要把 ValueTask 转成调用方需要的 Task。
.NET 10 的 ASP.NET Core 在 src/Shared/ValueTaskExtensions/ValueTaskExtensions.cs 中用 GetAsTask 处理无返回值的 ValueTask<FlushResult>:
public static Task GetAsTask(this in ValueTask<FlushResult> valueTask)
{
if (valueTask.IsCompletedSuccessfully)
{
valueTask.GetAwaiter().GetResult();
return Task.CompletedTask;
}
return valueTask.AsTask();
}
这两个分支分别处理不同的情况:
- 快速路径:同步成功时,调用
GetResult()消费ValueTask,再返回共享的Task.CompletedTask。这样可以避免调用AsTask();当ValueTask由IValueTaskSource支撑时,AsTask()可能需要分配一个Task。 - 其余情况:未完成、失败或取消时,
AsTask()返回一个能够表示后续结果、异常或取消的Task,满足方法返回Task的约定。
即使这里没有业务结果,也必须调用 GetResult()。它不只是“读取返回值”,对于一些由可复用 IValueTaskSource 支撑的操作,它还表示这次 ValueTask 已经被消费。因此不能因为操作已经完成,就直接跳过它。
如果外层方法本身就是 async,也没有必须返回 Task 的要求,通常仍然写:
await valueTask;
而不是 await valueTask.AsTask()。后者可能引入不必要的 Task 转换。只有返回类型、组合 API(例如 Task.WhenAll)或框架接口明确需要 Task 时,才应该转换。
ASP.NET Core MVC 的 Action 返回值
在 .NET 10 的 ASP.NET Core MVC 中,src/Mvc/Mvc.Core/src/Infrastructure/ControllerActionInvoker.cs 处理 Action 方法返回的 ValueTask<IActionResult> 时,采用的是“同步保存结果,异步时再等待”的写法:
private Task InvokeActionMethodAsync()
{
var objectMethodExecutor = _cacheEntry.ObjectMethodExecutor;
var actionMethodExecutor = _cacheEntry.ActionMethodExecutor;
var orderedArguments = PrepareArguments(_arguments, objectMethodExecutor);
var actionResultValueTask = actionMethodExecutor.Execute(
ControllerContext, _mapper, objectMethodExecutor, _instance!, orderedArguments);
if (actionResultValueTask.IsCompletedSuccessfully)
{
_result = actionResultValueTask.Result;
}
else
{
return Awaited(this, actionResultValueTask);
}
return Task.CompletedTask;
static async Task Awaited(
ControllerActionInvoker invoker,
ValueTask<IActionResult> actionResultValueTask)
{
invoker._result = await actionResultValueTask;
}
}
如果 Action 方法同步返回了 IActionResult,MVC 就直接保存结果并返回 Task.CompletedTask。只有 Action 方法尚未完成时,才调用局部异步方法等待。失败和取消也会走这条路径,因此异常仍然由 await 按正常的异步语义传递。
ASP.NET Core MVC 的资源释放
同一个 ResourceInvoker 在释放资源时返回 ValueTask。虽然这里没有结果可读取,但它同样会通过 IsCompletedSuccessfully 判断是否需要等待:
internal ValueTask ReleaseResourcesCore(IDisposable? scope)
{
Exception? releaseException = null;
ValueTask releaseResult;
try
{
releaseResult = ReleaseResources();
if (!releaseResult.IsCompletedSuccessfully)
{
return HandleAsyncReleaseErrors(releaseResult, scope);
}
}
catch (Exception exception)
{
releaseException = exception;
}
return HandleReleaseErrors(scope, releaseException);
static async ValueTask HandleAsyncReleaseErrors(
ValueTask releaseResult, IDisposable? scope)
{
Exception? releaseException = null;
try
{
await releaseResult;
}
catch (Exception exception)
{
releaseException = exception;
}
await HandleReleaseErrors(scope, releaseException);
}
}
官方代码:ResourceInvoker.cs。释放操作同步成功时,代码继续执行后续的释放逻辑;失败、取消和未完成时,则交给局部异步方法,让 await 统一处理异常。
HttpResponse.WriteAsync 中的连续快速路径
HttpResponseWritingExtensions.WriteAsync 中有连续两层这样的判断。它先检查 StartAsync 是否同步完成;如果完成,就继续写入。写完后,再把 FlushAsync 返回的 ValueTask<FlushResult> 交给前面的 GetAsTask。
public static Task WriteAsync(
this HttpResponse response,
string text,
Encoding encoding,
CancellationToken cancellationToken = default)
{
if (!response.HasStarted)
{
var startAsyncTask = response.StartAsync(cancellationToken);
if (!startAsyncTask.IsCompletedSuccessfully)
{
return StartAndWriteAsyncAwaited(
response, text, encoding, cancellationToken, startAsyncTask);
}
}
Write(response, text, encoding);
return response.BodyWriter.FlushAsync(cancellationToken).GetAsTask();
}
private static async Task StartAndWriteAsyncAwaited(
HttpResponse response,
string text,
Encoding encoding,
CancellationToken cancellationToken,
Task startAsyncTask)
{
await startAsyncTask;
Write(response, text, encoding);
await response.BodyWriter.FlushAsync(cancellationToken);
}
这个例子说明,快速路径不一定只有一层。每一步都可以先处理同步完成的情况;一旦遇到真正未完成的操作,就调用局部异步方法,后面的代码仍然按普通的 await 写法执行。
基准测试:同步成功的 ValueTask
下面给出完整的基准代码。在 .NET 10 控制台项目中引用 BenchmarkDotNet 后,执行下面的命令即可运行:
dotnet run -c Release
项目只需要引用 BenchmarkDotNet:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="0.15.8" />
</ItemGroup>
</Project>
完整基准代码如下:
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
using BenchmarkDotNet.Running;
using System.Threading.Tasks.Sources;
BenchmarkRunner.Run<ValueTaskAwaitBenchmarks>();
[MemoryDiagnoser]
[SimpleJob(launchCount: 1, warmupCount: 5, iterationCount: 12, invocationCount: 10_000_000)]
public class ValueTaskAwaitBenchmarks
{
private readonly CompletedValueTaskSource _source = new();
[Benchmark(Baseline = true)]
public async ValueTask DirectAwait()
{
await _source.CreateCompleted();
}
[Benchmark]
public ValueTask FastPath()
{
var valueTask = _source.CreateCompleted();
if (valueTask.IsCompletedSuccessfully)
{
valueTask.GetAwaiter().GetResult();
return ValueTask.CompletedTask;
}
return AwaitSlowPath(valueTask);
}
[Benchmark]
public Task ConvertToTask()
{
return _source.CreateCompleted().AsTask();
}
[Benchmark]
public Task FastPathToTask()
{
var valueTask = _source.CreateCompleted();
if (valueTask.IsCompletedSuccessfully)
{
valueTask.GetAwaiter().GetResult();
return Task.CompletedTask;
}
return valueTask.AsTask();
}
private static async ValueTask AwaitSlowPath(ValueTask valueTask)
{
await valueTask;
}
}
public sealed class CompletedValueTaskSource : IValueTaskSource
{
private ManualResetValueTaskSourceCore<bool> _core;
public ValueTask CreateCompleted()
{
_core.Reset();
_core.SetResult(true);
return new ValueTask(this, _core.Version);
}
void IValueTaskSource.GetResult(short token) => _core.GetResult(token);
ValueTaskSourceStatus IValueTaskSource.GetStatus(short token) =>
_core.GetStatus(token);
void IValueTaskSource.OnCompleted(
Action<object?> continuation,
object? state,
short token,
ValueTaskSourceOnCompletedFlags flags) =>
_core.OnCompleted(continuation, state, token, flags);
}
测试使用 ManualResetValueTaskSourceCore<T> 模拟由 IValueTaskSource 支撑、但调用时已经成功完成的可复用操作。这正是 IsCompletedSuccessfully 可能带来收益的场景。真正还没有完成的 I/O 必然需要注册后续回调,这部分成本不在本次基准的范围内。
比较的四种写法如下:
| 方法 | 行为 |
|---|---|
DirectAwait |
async ValueTask 方法中直接 await valueTask。 |
FastPath |
成功时 GetResult() 并返回 ValueTask.CompletedTask;否则调用局部异步方法。 |
ConvertToTask |
直接调用 valueTask.AsTask()。 |
FastPathToTask |
成功时 GetResult() 并返回 Task.CompletedTask;否则调用 AsTask()。 |
运行环境:macOS Sequoia 15.7.7、Apple Silicon arm64、.NET SDK 10.0.100、.NET 10.0.0、BenchmarkDotNet 0.15.8。每轮一千万次调用,预热 5 轮、测量 12 轮。
| 方法 | Mean | 相对 DirectAwait | 托管分配 |
|---|---|---|---|
DirectAwait |
15.71 ns | 1.00 | 0 B |
FastPath |
13.48 ns | 0.86 | 0 B |
ConvertToTask |
12.90 ns | 0.82 | 0 B |
FastPathToTask |
12.85 ns | 0.82 | 0 B |
FastPath 比直接 await 少约 2.2 ns,约为 14%。不过这个数字本身很小,而且直接 await 也没有分配。这个结果并不意味着所有 ValueTask 都应该先检查完成状态。只有 MVC、Kestrel 这类调用频率很高、同步成功又很常见的框架代码,才值得用额外分支去换这点成本。
本次测试中的 ConvertToTask 也没有分配,但这不代表所有 AsTask() 调用都如此。ValueTask 的实际来源会影响转换成本,特别是由某些 IValueTaskSource 实现支撑时,AsTask() 仍可能创建一个 Task。因此,只要 API 不要求 Task,还是应该直接 await valueTask。
总结
对大多数调用方来说,直接写下面这行代码即可:
var result = await valueTask;
await 自己会判断操作是否完成。手动检查 IsCompleted 通常不会减少工作量,还需要额外处理失败和取消的情况。
一个 ValueTask 实例只应消费一次。不要多次 await,也不要多次调用 AsTask()。如果需要多个等待者,或者需要将结果交给 Task.WhenAll 之类的 API,应当调用一次 AsTask() 并保存返回的 Task。
不要为了 await 而调用 AsTask()。只有方法签名、框架接口或组合 API 明确要求 Task 时,才需要转换。
IsCompletedSuccessfully 主要用于 .NET runtime、ASP.NET Core 这类调用频率很高的基础代码。它表示操作已经成功完成,可以安全读取 Result。普通业务代码无需模仿这种写法;只有性能分析已经证明同步成功路径是热点时,才值得用额外分支换取那一点收益。

浙公网安备 33010602011771号