0Pricing
C# Academy · 课时

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 throughput

ValueTask 的限制:只能等待一次

关键规则: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 times

ValueTask<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 hit

SocketsHttpHandler 中的 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 反馈 — 无需本地设置。

此课程中的所有课时

  1. IAsyncEnumerable 与 await foreach
  2. System.Threading.Channels
  3. ValueTask 与避免分配
  4. ConfigureAwait 与同步上下文
← 返回 C# Academy