用 ValueTask
替代 Task
时要注意同步完成路径
同步返回的异步方法(比如缓存命中、参数校验失败)用
ValueTask能避免堆分配,但前提是不跨 await 边界重用或暴露给外部。常见错误是把
ValueTask存进字段、传给非 await 上下文(如
ContinueWith),或在未 await 前多次 await —— 这会抛
InvalidOperationException: "ValueTask may only be awaited once"。
实操建议:
只在 hot path(如高频 API 入口、内存缓存读取)中对确定「大概率同步完成」的方法用ValueTask避免在公共 API 返回
ValueTask,除非你完全控制调用方行为;内部方法可放宽 若需复用或延迟 await,先转成
Task:
var task = valueTask.AsTask();别用
ValueTask.ConfigureAwait(false)—— 它不存在,必须先转
Task
异步流处理优先选 IAsyncEnumerable<t></t>
,但别在循环里开新 async
方法
IAsyncEnumerable<t></t>是 C# 8+ 处理高并发数据流(如分页查库、实时日志推送)的自然选择,但它本身不解决并发度控制。常见陷阱是写成这样:
await foreach (var item in GetItemsAsync()) // 每次 yield 都可能触发一次 DB 查询
{
await ProcessItemAsync(item); // 串行执行,吞吐掉一半
}正确做法是用
Task.WhenAll控制并发批次,同时保持流式内存友好: 用
BufferBlock<t></t>(来自
System.Threading.Tasks.Dataflow)做生产者-消费者解耦 限制并行度:用
Parallel.ForEachAsync(.NET 6+)并设
MaxDegreeOfParallelism若仍用
await foreach,提前批量化:
var batch = items.Take(100).ToList(),再
Task.WhenAll(batch.Select(ProcessItemAsync))
锁不是唯一瓶颈,ConcurrentDictionary
和 ImmutableArray
往往更轻量
高并发下盲目加
lock或
Monitor容易成为争用热点,尤其在短临界区(如更新计数器、查缓存)。
ConcurrentDictionary的
GetOrAdd和
AddOrUpdate是无锁设计,比手动 lock + Dictionary 快 3–5 倍(实测 .NET 6+)。
但要注意:
ConcurrentDictionary的枚举不是线程安全快照,遍历时可能漏项或重复;需要完整快照就用
ToArray()高频小对象拼接(如日志上下文构建)用
ImmutableArray<t>.Builder</t>比
List<t></t>+ lock 更省 GC 纯读多写少场景,考虑
Lazy<t></t>+
ConcurrentDictionary组合,避免重复初始化
可读性妥协点要显式标注,比如用 [SkipLocalsInit]
或内联 Span<byte></byte>
极致性能优化(如零分配序列化、Socket 缓冲区复用)必然牺牲可读性。这时别藏技巧,用编译器特性或注释明确意图:
用[SkipLocalsInit]省掉栈上数组初始化开销,但必须确保所有分支都赋值,否则行为未定义 字符串解析优先用
ReadOnlySpan<char></char>+
Span<byte></byte>,避免
Encoding.UTF8.GetBytes(str)分配;但只在 hot path 用,普通逻辑保持
string在方法名或 XML 注释里写清权衡:“// ⚠️ 零分配,但要求 input.Length
最难的不是写出高性能代码,而是让半年后的自己或同事一眼看出哪行是“为吞吐让步”,哪行是“真不能动”。可读性和性能冲突时,边界一定要划清楚——模糊地带最容易出偶发超时或内存泄漏。
