ValueTask 是什么,什么时候该用它而不是 Task
ValueTask是 C# 7.0 引入的轻量级异步返回类型,本质是
struct,避免了堆分配。它适合「大概率同步完成」的异步操作(比如内存缓存命中、简单计算、已就绪的 I/O),而
Task是
class,每次调用都产生 GC 压力。
常见误用场景:把所有
async方法都改成返回
ValueTask。这反而可能出问题——比如方法体里用了
await多次、或捕获了
this(导致状态机被装箱),
ValueTask就会退化为
Task,还多了一层间接开销。 ✅ 推荐:I/O 操作前先查本地缓存(如
MemoryCache)、配置读取、简单数据转换等「热路径」 ❌ 避免:涉及数据库查询、HTTP 调用、长时间
await的方法,直接用
Task⚠️ 注意:
ValueTask不可重复等待(
await两次会抛
InvalidOperationException),也不能直接调用
.Result或
.Wait()
如何正确声明和返回 ValueTask
关键不是加个
ValueTask类型就完事,得配合
ValueTask<t></t>构造方式和底层实现逻辑。
最安全的写法是:用
ValueTask.FromResult(T)返回同步结果,或用
new ValueTask<t>(task)</t>包装已有
Task<t></t>;但更推荐让底层 API 直接支持(比如
Stream.ReadAsync在 .NET 5+ 已返回
ValueTask<int></int>)。 同步快速返回:用
return ValueTask.FromResult(result);异步分支:用
return new ValueTask<t>(SomeAsyncMethod());</t>(注意这里仍会分配
Task,但只在真异步时发生) 不要手动 new 状态机:别写
return new ValueTask<t>(new AsyncStateMachine());</t>—— 编译器不认,且无法复用 协程式写法?C# 没有原生协程,所谓「协程异步」实际是
async/await状态机 +
ValueTask优化,不是 Unity 那种
yield return
ValueTask 和 async/await 搭配时的性能陷阱
编译器对
async方法返回
ValueTask的优化很敏感,稍不注意就失效。
典型破防点:方法里哪怕只有一处
await后面又用了
ConfigureAwait(false),或者 await 了多个不同来源的
ValueTask,编译器就可能放弃结构体优化,生成带装箱的状态机。 ✅ 安全模式:单个
await,且 await 的对象本身是
ValueTask(如
MemoryStream.ReadAsync) ❌ 危险信号:方法里出现
await x; await y;(两个独立
ValueTask)、或
await task.ConfigureAwait(false)(
ConfigureAwait只对
Task有效,对
ValueTask无意义,还触发装箱) ? 验证是否真省了 GC:用
dotnet trace抓
GC-Collect事件,对比改前后
Gen0分配次数;或看反编译后的状态机类型是不是
struct
协程风格代码在 C# 里怎么写才不踩坑
C# 没有语言级协程,所谓「协程异步」基本是开发者对
async/await的心理投射。强行模拟 yield-return 式协程(比如用
Channel<t></t>+ 循环
await reader.ReadAsync())容易写出高延迟、难调试的流式逻辑。
真正需要分阶段执行的场景,优先考虑组合现有异步原语:
用IAsyncEnumerable<t></t>替代手写「协程迭代器」(如分页拉数据) 用
Task.WhenAll/
Task.WhenAny控制并发节奏,比自己管理 yield 点更可靠 如果必须暂停恢复,用
ManualResetValueTaskSourceCore<t></t>手动控制
ValueTask完成时机(高级用法,仅限极低延迟组件) 别为了“协程感”而拆分过细的
async方法——每层调用都增加状态机开销,反而抵消
ValueTask的收益
最常被忽略的一点:
ValueTask的价值不在单次调用快多少,而在高频调用下减少 Gen0 GC。如果你的接口 QPS 不到几百,优化它不如先检查序列化、数据库连接池或锁竞争。
