ValueTask 被 await 多次会抛出 InvalidOperationException
直接 await 同一个
ValueTask实例两次,运行时大概率会触发异常:
System.InvalidOperationException: "The ValueTask may only be awaited once."。这不是未定义行为,而是 .NET 在
ValueTask内部做了明确检查 —— 它不是设计来支持重复消费的。 底层靠
ManualResetValueTaskSourceCore<t></t>或类似机制实现时,首次 await 会标记“已获取”,再次 await 就直接 throw 即使该
ValueTask包装的是已完成的
Task(比如
ValueTask.FromResult(42)),也仍受此限制 —— 因为它内部可能持有一个可重用的
Task,但
ValueTask本身仍是单次语义 只有极少数情况(如某些同步完成且无状态的
ValueTask)可能不抛异常,但这是实现细节,不可依赖
ValueTask 和 Task 在重复 await 上的行为差异
Task可以安全地多次 await:它本身是“热”的、可共享的;而
ValueTask是“冷”的、一次性资源,设计目标是避免堆分配,代价就是放弃可重用性。
await task;+
await task;→ 正常,第二次 await 立即返回结果
var vt = new ValueTask<int>(42); await vt; await vt;</int>→ 第二次 await 抛异常 如果需要多次等待,必须显式转换:用
vt.AsTask()得到一个可重用的
Task,但会触发一次堆分配(失去
ValueTask的零分配优势)
如何安全地多次使用同一个异步结果
核心原则:不要保存
ValueTask变量后反复 await;要么转成
Task,要么把结果提取出来再复用。 想“等一次、用多次”:先
await,再存结果值 ——
int result = await GetValueAsync(); // ValueTask<int><br>Console.WriteLine(result);<br>Console.WriteLine(result * 2);想“多次触发 await 行为”(比如重试逻辑):每次调用都重新获取新的
ValueTask实例 ——
for (int i = 0; i < 3; i++) {<br> try {<br> await DoWorkAsync(); // 每次都是新 ValueTask<br> break;<br> } catch { /* ... */ }<br>}
必须传给多个消费者且都要 await:用 .AsTask(),接受分配开销 ——
var vt = GetOperation();<br>var t = vt.AsTask();<br>await t;<br>await t; // OK
容易被忽略的隐式多次 await 场景
有些写法看似只 await 了一次,实则在编译或运行时触发了多次 —— 特别要注意 async 方法体内的
await表达式求值顺序和捕获上下文的副作用。 LINQ 查询中误用:
var tasks = list.Select(x => DoAsync(x));<br>await Task.WhenAll(tasks); // 这里每个 DoAsync(x) 返回新 ValueTask,没问题<br>// ❌ 但如果写成 list.Select(_ => vt).ToArray(),就真在复用同一个 vt属性 getter 返回
ValueTask:每次调用 getter 应返回新实例;若缓存了
ValueTask字段并反复返回它,就会踩坑 调试时在 Watch 窗口输入
await vt:VS 调试器会真实执行 await,导致后续代码中的 await 失败
重复 await 一个
ValueTask不是边界情况,而是明确禁止的操作。它的“一次性”是契约级约束,不是优化副作用。只要变量生命周期跨过一次 await,就该把它当成已消耗掉的资源。
