ValueTask 与避免分配
在热点路径中使用 ValueTask 减少堆分配,了解其限制,并避免常见的误用陷阱。
ValueTask 与避免分配 是 CoddyKit 上的免费 C# Academy 课时。 这是第 3 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 C# Academy 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 C# Academy 课程共包含 4 节课。
Task 的分配问题
Task<T> 是引用类型——每个 async 方法都会在堆上分配一个 Task,即使结果可以同步获得(例如来自缓存)。在热点路径中,这可能造成显著的 GC 压力。ValueTask<T> 可以解决这个问题。
什么是 ValueTask?
ValueTask<T> 是一种结构,可以表示同步可用的结果或待处理的异步操作。当结果可以同步获得时,无需进行堆分配。
// Task<T>: always allocates, even when result is cached
public async Task<string> GetCached_Task() =>
_cache.TryGetValue("key", out var v) ? v : await FetchAsync();
// ValueTask<T>: zero allocation when value is cached
public ValueTask<string> GetCached_ValueTask() =>
_cache.TryGetValue("key", out var v)
? ValueTask.FromResult(v) // no allocation
: new ValueTask<string>(FetchAsync()); // allocation only when needed从接口返回 ValueTask
对于调用频繁且经常同步完成的接口方法,请使用 ValueTask<T>。consumer 的使用方式与 Task<T> 完全相同。
public interface IProductRepository
{
// ValueTask: often completes synchronously from cache
ValueTask<Product?> GetByIdAsync(int id);
// Task: always async (DB query)
Task<List<Product>> SearchAsync(string query);
}
// Consumption is identical to Task:
var product = await _repo.GetByIdAsync(42);
var results = await _repo.SearchAsync("keyboard");ValueTask 与 IValueTaskSource
在极致性能场景下,可以实现 IValueTaskSource<T> 来池化后备状态对象。这样即使异步操作完成,也能避免所有分配;.NET 的套接字和 I/O 代码就在内部采用了这种方式。
// In most cases, use the simple ValueTask approach.
// IValueTaskSource is for library authors needing zero-alloc async at extreme scale.
// Example: System.IO.Pipelines uses this internally
var result = await pipe.Reader.ReadAsync(ct);
// ReadAsync returns ValueTask<ReadResult> backed by IValueTaskSource
// -- zero allocation for every read even under high throughputValueTask 的限制:只能等待一次
关键规则:ValueTask<T> 最多只能被等待一次,并且在其完成之前不能调用 .Result。违反此规则会导致未定义行为。
var vt = repo.GetByIdAsync(42);
// BAD: await multiple times
var a = await vt; // OK
var b = await vt; // UNDEFINED BEHAVIOR — don't do this!
// BAD: two concurrent awaits
var t1 = vt.AsTask(); // converts to Task (safe to share)
var t2 = vt.AsTask(); // second AsTask() on same ValueTask — WRONG
// GOOD: if you need multiple awaits, convert to Task
var task = repo.GetByIdAsync(42).AsTask();
var r1 = await task;
var r2 = await task; // OK — Task can be awaited multiple timesValueTask<T> 与 Task<T> 的选择指南
只有在性能分析显示异步方法存在分配压力时,才使用 ValueTask<T>。请勿在所有地方使用它——对于低频代码,它会增加复杂性却没有收益。
// USE ValueTask when:
// 1. The method often completes synchronously (cache hit)
// 2. The method is on a hot path called millions of times/sec
// 3. You're writing a library with high-throughput I/O
// USE Task when:
// 1. The result is always async
// 2. The method might be awaited multiple times
// 3. You use Task.WhenAll / Task.WhenAny
// 4. You store the task in a variable and share it非泛型 ValueTask
非泛型 ValueTask 适用于返回 Task 的方法(无返回值 async)。对于调用频繁且经常同步完成的方法,可以使用它。
public interface ICache
{
// Often a no-op when value exists — ValueTask avoids allocation
ValueTask SetAsync(string key, string value);
ValueTask InvalidateAsync(string key);
}
public class InMemoryCache : ICache
{
private readonly Dictionary<string, string> _data = new();
public ValueTask SetAsync(string key, string value)
{
_data[key] = value;
return ValueTask.CompletedTask; // no allocation
}
}使用 BenchmarkDotNet 测量分配
在 BenchmarkDotNet 中使用 [MemoryDiagnoser] 测量每次操作的分配量。这是验证 ValueTask 是否确实有帮助的唯一可靠方式。
[MemoryDiagnoser]
public class CacheBenchmark
{
private readonly CachedRepo _repo = new();
[Benchmark]
public async Task<Product?> Task_GetById()
=> await _repo.GetByIdTask(1);
[Benchmark]
public async ValueTask<Product?> ValueTask_GetById()
=> await _repo.GetByIdValueTask(1);
}
// Run: dotnet run -c Release
// Compare Gen0 allocations column — ValueTask should show 0B on cache hitSocketsHttpHandler 中的 ValueTask
.NET HTTP 堆栈在内部广泛使用 ValueTask。了解这一点有助于您编写高性能 HTTP 客户端,使其与运行时的内存分配策略保持一致。
// HttpClient uses ValueTask-based sockets internally.
// When using HttpClient.GetAsync(), the response is awaited with Task<HttpResponseMessage>.
// For streaming large bodies, use:
using var response = await client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead);
await using var stream = await response.Content.ReadAsStreamAsync();
// Stream reads internally use ValueTask<int> for zero-allocation I/O实际案例:热点路径缓存服务
缓存服务的每次调用都可能命中缓存——ValueTask 可以在热点路径上消除分配,同时仍支持缓存未命中时的异步回退。
public class ProductCache
{
private readonly Dictionary<int, Product> _cache = new();
private readonly IProductRepository _repo;
public ProductCache(IProductRepository repo) => _repo = repo;
public ValueTask<Product?> GetAsync(int id)
{
// Cache hit: no allocation
if (_cache.TryGetValue(id, out var cached))
return ValueTask.FromResult<Product?>(cached);
// Cache miss: async fetch
return new ValueTask<Product?>(FetchAndCacheAsync(id));
}
private async Task<Product?> FetchAndCacheAsync(int id)
{
var product = await _repo.GetByIdAsync(id);
if (product is not null) _cache[id] = product;
return product;
}
}快速检查
关于等待 ValueTask
回顾:ValueTask 与避免分配
要点:
ValueTask<T>:当结果可以同步获得时,可避免堆分配的结构- 适用于经常同步完成的热点路径(缓存命中)
- 请勿多次等待
ValueTask<T>——使用.AsTask()进行共享 - 在优化前使用
[MemoryDiagnoser]验证分配节省效果 - 通常优先使用
Task<T>;只有性能分析证明有必要时才使用ValueTask<T>
常见问题解答
「ValueTask 与避免分配」课时是免费的吗?
是的 — 「ValueTask 与避免分配」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 C# Academy 课程的其余内容,请升级到 CoddyKit PRO。 C# Academy 课程共包含 4 节课。
「ValueTask 与避免分配」这节课中我会学到什么?
在热点路径中使用 ValueTask 减少堆分配,了解其限制,并避免常见的误用陷阱。 你通过在浏览器中直接运行的动手代码来练习 C# Academy,全天候 AI 导师会在你学习这节课的过程中回答你的问题。
学习 C# Academy 需要有经验吗?
无需任何先前经验。CoddyKit 上的 C# Academy 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 3 节课,共 4 节。
「ValueTask 与避免分配」课时需要多长时间?
大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。
我能在这节 C# Academy 课中编写并运行代码吗?
能。每节 C# Academy 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。