文章

C# 中什么时候该用 Task,什么时候该用 ValueTask?

C# 中什么时候该用 Task,什么时候该用 ValueTask?

在现代 C# / .NET 开发中,async/await 已经成为日常编码的一部分,而 Task 几乎是所有异步方法的默认返回类型。

随着 .NET 的演进,ValueTask 被引入,很多开发者开始困惑:

  • 我是不是应该把所有 Task 都换成 ValueTask?
  • 返回 bool 这么简单的结果,用 ValueTask<bool> 会不会更高效?
  • 业务代码中到底该不该用 ValueTask?

这篇文章会系统地讲清楚 Task 与 ValueTask 的区别、联系、适用场景以及常见误区,给你一套可直接用于代码评审和架构设计的判断标准。


一、一句话结论(先给答案)

  • ✅ 绝大多数场景:使用 Task / Task<T>
  • ⚠️ 只有在明确的性能瓶颈下,才考虑 ValueTask / ValueTask<T>
  • ❌ 返回的是 bool,并不是使用 ValueTask<bool> 的理由

ValueTask 是性能优化工具,而不是新的默认标准。


二、Task 与 ValueTask 的本质区别

1. Task 是什么?

  • 引用类型(class)
  • 表示一个“可能异步完成”的操作
  • 即便方法同步完成,也需要:

    • 分配一个 Task 对象,或
    • 使用缓存的 Task(如 Task.CompletedTask、Task.FromResult)
1
2
3
4
Task<bool> CheckAsync()
{
    return Task.FromResult(true);
}

优点很明显:

  • 语义清晰
  • API 丰富
  • 使用安全
  • 几乎不会被误用

2. ValueTask 是什么?

  • 值类型(struct)
  • 内部可以:

    • 直接保存同步结果
    • 或包装一个 Task
  • 设计目标是:

在“高频且同步完成”的场景下,避免不必要的 Task 对象分配

1
2
3
4
ValueTask<bool> CheckAsync()
{
    return new ValueTask<bool>(true);
}

三、最容易被忽略,但最重要的区别

1️⃣ ValueTask 默认只能被 await 一次

1
2
3
4
ValueTask<int> vt = GetAsync();

await vt; // ✅
await vt; // ❌ 未定义行为,可能直接抛异常

而 Task 可以被安全地多次 await。

这意味着:

  • ValueTask 不适合作为公共 API 返回类型
  • 非常容易被调用方误用

2️⃣ 组合操作对 ValueTask 非常不友好

下面这些对 Task 来说是家常便饭:

1
2
await Task.WhenAll(task1, task2);
await Task.WhenAny(task1, task2);

而 ValueTask:

  • 没有 WhenAll(ValueTask)
  • 通常只能 .AsTask() 再参与组合
1
await Task.WhenAll(vt1.AsTask(), vt2.AsTask());

一旦 AsTask():

ValueTask 的性能优势直接归零


3️⃣ ValueTask 增加了心智负担

使用 ValueTask 时,你必须额外确保:

  • 不被多次 await
  • 不被缓存
  • 不被随意传递

在业务代码中,这种复杂度通常是得不偿失的。


四、什么时候“应该”使用 ValueTask?

唯一值得推荐的核心场景

高频调用 + 大概率同步完成 + 性能敏感


场景一:底层库 / 基础设施代码

.NET BCL 中的经典例子:

1
ValueTask<int> ReadAsync(Memory<byte> buffer);

原因:

  • 数据可能已经在内存中
  • 不需要真正的异步 I/O
  • 每秒调用次数极高

场景二:缓存命中率极高的逻辑

1
2
3
4
5
6
7
ValueTask<User?> GetUserAsync(int id)
{
    if (_cache.TryGetValue(id, out var user))
        return new ValueTask<User?>(user);

    return new ValueTask<User?>(LoadFromDbAsync(id));
}

这里的价值在于:

  • 缓存命中:0 分配
  • 缓存未命中:退化为 Task

五、什么时候不应该使用 ValueTask?

❌ 1. 业务代码 / Controller / Application Service

1
public ValueTask<bool> CreateOrderAsync() // ❌ 不推荐

业务层更重要的是:

  • 可读性
  • 可维护性
  • 可组合性

而不是极限性能。


❌ 2. 明确的 I/O 异步操作

例如:

  • HTTP 请求
  • 数据库访问
  • 文件系统操作
1
Task<bool> SaveAsync() // ✅

这些场景中,ValueTask 几乎没有任何收益。


❌ 3. 公共 API / SDK 接口

除非你能完全控制调用方式,否则:

1
Task<T> // 永远是更安全的选择

六、重点问题:返回 bool,要不要用 ValueTask?

答案:不需要,绝对不是

是否使用 ValueTask<bool>,与返回类型是不是 bool 没有任何直接关系。

唯一的判断依据是:

这个方法是否高频调用,且大多数情况下同步完成?


正确示例(可考虑 ValueTask)

1
2
3
4
5
6
7
ValueTask<bool> IsFeatureEnabledAsync()
{
    if (_configCached)
        return new ValueTask<bool>(true);

    return new ValueTask<bool>(LoadFromRemoteAsync());
}

错误示例(常见误用)

1
2
3
4
5
// ❌ 只是因为返回 bool
public ValueTask<bool> ExistsAsync(int id)
{
    return new ValueTask<bool>(_db.ExistsAsync(id));
}

这个写法:

  • 没有减少分配
  • 增加了复杂度
  • 没有任何性能收益

七、推荐的实践准则(可直接用于 Code Review)

✔ 默认规则

99% 的 async 方法:使用 Task / Task<T>


✔ 只有在同时满足以下条件时,才考虑 ValueTask

  • 调用频率极高(例如每秒成千上万次)
  • 大多数情况下同步完成
  • 性能分析工具(Benchmark / Profiler)证明有价值
  • 调用方完全可控

八、总结

  • Task 是安全、易用、可组合的通用抽象
  • ValueTask 是为极端性能场景准备的优化工具
  • 不要为了“看起来高级”而使用 ValueTask

先写对,再写快; 没有性能数据支撑的优化,通常都是负优化。

如果你在 ASP.NET Core、ABP、或者高并发服务中遇到具体场景,不确定是否该用 ValueTask,那往往意味着:

👉 你应该继续用 Task。

本文由作者按照 CC BY 4.0 进行授权