CodeGym /课程 /C# SELF /性能优化: ValueTask

性能优化: ValueTask

C# SELF
第 62 级, 课程 2
可用

1. 介绍

在真正写代码之前,先用通俗的方式理解一下。记住,在 .NET 里创建一个对象(更别提 task 了!)会占用内存并消耗一点 CPU 时间。现在想象一个异步 API,其中一半情况下结果是立刻可用的(比如从缓存里取),另一半情况需要访问数据库,因此操作是真正异步的并且返回 Task。这种情况很常见——比如文件缓存、网络 API、对象池以及其他“异步但有时瞬时”的场景。

如果我们总是返回 Task,即使结果是瞬时的,我们也得多创建对象。要是能在结果已经准备好的时候直接返回结果而不用包装成 task 会怎么样?这就是 ValueTask 出现的原因。

事实: 标准的 Task.CompletedTask 和 Task.FromResult(…) 确实能节省一些执行时间,但它们也会创建共享对象,在高并发场景下可能不是最优的。

什么是 ValueTask

ValueTask 是一个特殊的 struct 包装,可以表示已经准备好的结果,或者(当操作真的异步时)包装一个 Task。简单来说,它是一个“包”,要么直接包含结果,要么包含指向 Task 的引用。

主要有两种形式:

  • ValueTask — 不返回值的(像 Task 那种)
  • ValueTask<TResult> — 包装返回值的(类似 Task<TResult>)

对比 Task 和 ValueTask

类型 分配次数 能同步完成吗 通常用于
Task
一次(heap) 是/否 几乎总是
ValueTask
零次/一次 是/否 优化用
ValueTask<T>
零次/一次 是/否 优化用

什么时候使用 ValueTask

有条黄金规则:如果你写的异步方法总是在经过真正的 async 操作后才返回结果,就用 Task。这更简单、安全、也更直观。

使用 ValueTask 值得考虑的情况:

  • 结果可能同步得到(比如从缓存、池或内存中),且你想避免额外分配。
  • 真正异步的情况不太频繁(否则优化收益会被代码复杂度和结构复制的开销抵消)。

注意! 如果你总是返回异步结果——直接使用 Task。如果你希望 API 对常见的瞬时结果做到“超优化”——可以考虑 ValueTask。

2. 同步还是异步结果

看一个查找用户名字的函数示例。如果用户在缓存中——立刻返回;如果不在——异步从“数据库”加载:


// 模拟我们的用户
public class User
{
    public string Name { get; set; }
}

// 我们的 cache(很简单)
private readonly Dictionary<string, User> _localCache = new();

public async ValueTask<User> FindUserAsync(string name)
{
    // 检查本地缓存
    if (_localCache.TryGetValue(name, out var user))
    {
        // 结果是瞬时的——不会分配 task!
        return user;
    }

    // 这里是假装的耗时异步操作(比如从数据库)
    user = await LoadUserFromDbAsync(name);
    // 放入缓存以备下次
    _localCache[name] = user;
    return user;
}

private async Task<User> LoadUserFromDbAsync(string name)
{
    // 模拟延迟
    await Task.Delay(500);
    return new User { Name = name };
}

重要: 在缓存命中的情况下我们返回普通结果——不会分配 task!只有在需要“取出”结果的时候,我们才会创建异步任务。

ValueTask 内部是怎么工作的

ValueTask 内部要么包含一个准备好的值,要么包含指向 Task 的引用:

ValueTask result = ValueTask.CompletedTask;
ValueTask<int> valueResult = new ValueTask<int>(42);
ValueTask<int> valueResult2 = new ValueTask<int>(Task.Run(() => 42));

当你用 await 时,编译器会处理这些情况:如果结果是瞬时的,就不会创建额外对象。

关于 await 和 ValueTask 的重要事项

async 方法和对 ValueTask 的 await 一般是兼容的:

public async ValueTask PingAsync()
{
    // ...
    await Task.Delay(10);
}

但是如果你保存了一个 ValueTask 实例并准备晚点再等待它,要记住:不能 对同一个实例做多次 await。Task 可以被多次等待,而 ValueTask 不行。

错误:对同一个 ValueTask 重复 await

ValueTask<int> task = ComputeAsync();

// 这是可以的
int a = await task;

// 这是错误!对同一个 ValueTask 实例做第二次 await 不被允许:
// int b = await task; // 不行!

3. 重写我们控制台应用的一部分

假设我们现在在做一个迷你读书器:部分文本已经缓存在内存,剩下的异步加载。用 ValueTask 优化获取书第一行的方法:

private readonly Dictionary<string, string> _bookCache = new();

public async ValueTask<string> GetFirstLineOfBookAsync(string title)
{
    if (_bookCache.TryGetValue(title, out var bookText))
    {
        var firstLine = bookText.Split('\n')[0];
        return firstLine;
    }

    // 假设异步下载书文本
    var downloadedBook = await DownloadBookTextAsync(title);
    _bookCache[title] = downloadedBook;
    return downloadedBook.Split('\n')[0];
}

private async Task<string> DownloadBookTextAsync(string title)
{
    // 模拟延迟(比如从网络下载)
    await Task.Delay(1000);
    return $"Book: {title}\nThis is the first line.\nSecond line...";
}

这样当书已经在缓存里时,ValueTask 节省了创建 task 的开销。

4. 有用的细节

标记:什么时候不必使用 ValueTask

如果你的 API 总是异步的(比如总是通过网络请求),就用 Task,更简单也更安全。

如何把 ValueTask 转成 Task,反之亦然

有时候 ValueTask 无法直接交给只接受 Task 的 API,这种情况下用 .AsTask():

ValueTask<int> valueTask = ComputeAsync();
Task<int> task = valueTask.AsTask();

如果要从 Task 创建 ValueTask —— 直接用 task 来构造 ValueTask:

Task<int> task = ComputeAsyncTask();
ValueTask<int> valueTask = new ValueTask<int>(task);

对比:ValueTask vs Task

特性
Task
ValueTask
重复 await 允许 不允许
快速响应时的分配 有 没有(如果结果是瞬时的)
兼容接口 广泛支持 需要额外包装
适用性 到处可用 仅用于优化
池(Pool) 使用共享池 不是,struct
简单程度 简单 更复杂

在真实场景中使用 ValueTask

public async ValueTask<string> GetMessageAsync(int id)
{
    if (_messageCache.TryGetValue(id, out var value))
        return value;

    var result = await LoadMessageFromDbAsync(id);
    _messageCache[id] = result;
    return result;
}

面试应用: 如果面试官问你如何进一步提升带缓存的异步 API 的性能,可以提到 ValueTask。

与 LINQ 和 IAsyncEnumerable<T> 的兼容示例

如果你想在异步 LINQ(IAsyncEnumerable<T>)中使用 ValueTask,在 .NET 中这是支持的(比如 ToListAsync 方法):

public async ValueTask<List<int>> GetPrimeNumbersAsync()
{
    // 模拟部分数字已计算,部分是异步计算
    // ... (示例省略以保持简洁)
    return new List<int> { 2, 3, 5, 7, 11 };
}

5. 总结与实现要点

  1. 当大部分时间结果是瞬时可得(比如来自缓存)时,使用 ValueTask 做优化。
  2. 不要无脑使用 ValueTask——会增加代码复杂度并引入错误。
  3. 不要对同一个 ValueTask 做多次 await。如果需要,先把它转换成 Task。
  4. 与 Task 的互操作主要靠 .AsTask()。
  5. 记住 ValueTask 是个 struct,复制时行为可能不同。

6. 使用 ValueTask 时的常见错误

错误 #1: 对同一个 ValueTask 实例重复做 await。ValueTask 是个 struct,不能被 await 两次。第二次 await 会导致异常或不正确的行为。

错误 #2: 在没有检查兼容性的情况下使用 ValueTask。有些第三方 API 需要 Task,直接传递 ValueTask 可能会出问题。

错误 #3: 不必要地使用 ValueTask。如果结果总是异步的,使用 ValueTask 只会让代码更复杂而无实际收益。

错误 #4: 复制 ValueTask(作为 struct)。被复制的结构在 await 时可能有意想不到的表现,务必操作原始实例。

2
任务
C# SELF, 第 62 级, 课程 2
已锁定
立即或异步返回结果
立即或异步返回结果
评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION