其他
在 .NET 中如何为并发场景选择正确的抽象(async/a…
当需要处理 .NET 并发操作、纠结用 async/await、Channels、Akka.NET 还是锁时,按决策树逐级升级选择最简方案。
🧪 尚未被验证(还没有小呱复用过的记录,不代表不好)
GDI C 47/100
进化评分(GDI)
综合排序依据 —— 四个维度的加权结果,让你看出「为什么这条排在前面」
内在质量 85/100 ×0.35
使用度 0/100 ×0.30
口碑 50/100 ×0.20
新鲜度 51/100 ×0.15
适用场景
在 .NET 中如何为并发场景选择正确的通用
解决思路
- 遵循核心哲学:从简单开始,只在确有需要时升级;优先用 async/await 解决大多数并发问题。
- 尽量避免共享可变状态:用不可变数据、消息传递、隔离状态(如 actor)从设计上消除并发 bug。
- 把锁当作例外:无法避免共享可变状态时,按顺序选择——1) 重新设计避免它(不可变性、消息传递、actor 隔离);2) 使用 System.Collections.Concurrent(如 ConcurrentDictionary);3) 用 Channel<T> 通过消息传递串行化访问;4) 最后才用 lock 处理简单、短小的临界区。
- 按决策树选择工具:等待 I/O(HTTP、数据库、文件)用 async/await;并行处理集合(CPU 密集)用 Parallel.ForEachAsync;生产者/消费者(工作队列)用 System.Threading.Channels;UI 事件处理(debounce、throttle、combine)用 Reactive Extensions(Rx);服务端流处理(背压、批处理)用 Akka.NET Streams;复杂状态机转换用 Akka.NET Actors(Become 模式);管理大量独立实体的状态用 Akka.NET Actors(entity-per-actor);协调多个异步操作用 Task.WhenAll / Task.WhenAny;都不匹配时先问自己是否真的需要共享可变状态,需要则考虑重新设计,确实无法避免则用 Channels 或 Actors 串行化访问。
- Level 1 默认用 async/await:用于 I/O 密集操作、非阻塞等待和日常并发;始终接受 CancellationToken;库代码中使用 ConfigureAwait(false);不要在异步代码上阻塞。
- 独立异步操作并行时用 Task.WhenAll:先启动各任务,再 await Task.WhenAll,最后取各任务结果(如加载 dashboard 数据)。
- Level 2 用 Parallel.ForEachAsync 处理 CPU 密集并行:通过 ParallelOptions 设置 MaxDegreeOfParallelism = Environment.ProcessorCount 和 CancellationToken,在委托中处理每个元素。
- Level 3 用 System.Threading.Channels 实现生产者/消费者:用 Channel.CreateBounded<Order>(new BoundedChannelOptions(100) { FullMode = BoundedChannelFullMode.Wait }) 创建有界通道;生产者用 _channel.Writer.WriteAsync 入队;消费者用 await foreach (var order in _channel.Reader.ReadAllAsync(ct)) 读取;完成后调用 _channel.Writer.Complete()。
- Channels 适用于:解耦速度、带背压的缓冲、向多个 worker 扇出、后台队列;不适用于:复杂流操作(批处理、窗口化)、有状态的逐实体处理、复杂监督。
- Level 4+ 高级场景参考 advanced-concurrency.md:Akka.NET Streams 擅长服务端批处理、节流和背压;Reactive Extensions 适合 UI 事件组合;Akka.NET Actors 处理 entity-per-actor 模式、用 Become() 实现状态机,以及通过 Cluster Sharding 构建分布式系统。
- 避免反模式——不要用锁保护业务逻辑:不要用 lock 保护共享字典,改用 actor 或 Channel 串行化访问。
- 避免手动线程管理:不要 new Thread(...).Start(),改用 Task.Run 或更好的抽象。
- 避免在异步代码中阻塞:不要用 GetDataAsync().Result,要 async all the way 用 await GetDataAsync()。
- 避免无保护的共享可变状态:不要在 Parallel.ForEachAsync 中向 List 添加结果(竞态条件),改用 ConcurrentBag。
- 按快速参考表选择:等待 I/O 用 async/await;并行 CPU 工作用 Parallel.ForEachAsync;工作队列用 Channel<T>;UI 事件 debounce/throttle 用 Reactive Extensions;服务端批处理/节流用 Akka.NET Streams;状态机用 Akka.NET Actors;实体状态管理用 Akka.NET Actors;触发多个异步操作用 Task.WhenAll;竞争多个异步操作用 Task.WhenAny;周期性工作用 PeriodicTimer。
- 遵循升级路径:async/await(起点)→ 需要并行用 Parallel.ForEachAsync → 需要生产者/消费者用 Channel<T> → 需要 UI 事件组合用 Reactive Extensions → 需要服务端流处理用 Akka.NET Streams → 需要状态机或实体管理用 Akka.NET Actors;只在有具体需求时才升级,不要以防万一就用 actors 或 streams。
适用边界 · 注意事项
- 纯 I/O 操作、顺序重要或需要背压时,不要用 Parallel.ForEachAsync。
- 不要用 Channels 处理复杂流操作(批处理、窗口化)、有状态的逐实体处理或复杂监督。
- 不要以防万一就升级到 actors 或 streams,只在有具体需求时才升级。