C#中的并发编程
C# 提供了丰富且强大的并发编程工具集。关键在于理解不同技术的适用场景追求响应和吞吐,asyncawait是你的基石。榨干CPU性能,TPLParallelPLINQTask) 是你的利器。安全共享数据,并发集合是你的护甲。控制并发洪流,SemaphoreSlim是你的闸门。
·
掌握了C#中的并发编程,就像是为你的应用程序装上了强大的引擎,能充分释放多核处理器的潜力,打造出响应迅捷、吞吐高效的系统。但同时,它也需要你谨慎驾驭,避免“竞态条件”、“死锁”这些陷阱。我会结合经验,为你梳理并发编程的知识体系、适用场景以及如何应对常见挑战。
为了让你能快速建立起整体认知,我先用一个表格来汇总C#中主要的并发编程技术及其核心特点:
| 技术类型 | 关键技术/模式 | 核心思想 | 主要优势 | 典型应用场景 |
|---|---|---|---|---|
| 异步编程 | async/await |
避免阻塞调用线程,在I/O操作等等待时释放线程去执行其他工作 | 高吞吐、高响应度、节省线程 | Web API、UI客户端、I/O密集型操作(文件、网络) |
| 任务并行库 (TPL) | Task, Task<T>, Parallel, Parallel LINQ (PLINQ) |
提供高级抽象,简化并行代码编写,高效利用多核 | 开发效率高、性能优化、支持取消和异常聚合 | CPU密集型计算、循环并行化、数据并行 |
| 并发集合 | ConcurrentQueue<T>, ConcurrentDictionary<TKey, TValue>等 |
提供线程安全的集合类型,专为多线程并发访问设计 | 简化共享数据访问、避免低级锁、性能优化 | 生产者-消费者队列、共享缓存、并行任务间协作 |
| 响应式编程 | Reactive Extensions (Rx.NET) | 将事件和数据流作为中心,进行声明式组合和过滤 | 处理复杂事件流、支持背压、时间相关操作 | 事件驱动的UI、传感器数据处理、实时消息系统 |
| 数据流编程 | TPL Dataflow | 构建网格(mesh)管道,数据在其中流动并被处理,连接了生产者与消费者 | 精细控制数据流和转换、支持并行和缓冲 | 管道式处理、生产者-消费者复杂变体 |
| Actor模型 | Orleans, Akka.NET | 通过异步消息传递在独立实体(Actor)间进行通信 | 高可扩展性、强隔离性、位置透明 | 分布式系统、高并发游戏服务器、CQRS/Event Sourcing |
⚙️ 核心并发编程工具箱详解
1. 异步编程 (Async/Await)
这是处理I/O密集型操作的利器。
- 工作机制:使用
async和await关键字。当await一个耗时操作时,该方法会挂起,将线程返回到线程池,直到耗时操作完成,后续代码才由线程池中的线程(可能是原线程也可能是新线程)继续执行。 - 为何高效:它不阻塞线程。在等待数据库响应、文件读写或网络请求时,宝贵的线程资源可以被用来处理其他请求,极大提升了应用的吞吐量和响应能力。
- 示例:
// 在Web API中异步处理HTTP请求 public class DataController : ControllerBase { [HttpGet] public async Task<IActionResult> GetAsync(int id) { // 模拟异步数据库查询 var data = await _repository.GetDataByIdAsync(id); return Ok(data); } }
2. 任务并行库 (TPL) 与数据并行
TPL是并行计算的现代化工具,让你能更轻松地使用多核。
Task:代表一个异步操作。Task.Run常用于将CPU密集型工作卸载到线程池线程。- 数据并行:
Parallel.For和Parallel.ForEach可以将一个循环的迭代自动分配到多个线程上并行执行。// 使用 Parallel.ForEach 进行数据并行计算 void ProcessImages(List<Image> images) { Parallel.ForEach(images, image => { ComputeIntensiveTransformation(image); // 耗时的图像处理 }); } - PLINQ:并行LINQ,只需在LINQ查询后加
.AsParallel(),即可尝试并行执行查询。var source = Enumerable.Range(1, 10000); var evenNumbers = source.AsParallel() .Where(x => x % 2 == 0) .ToArray(); // 并行查询偶数
3. 并发集合 (Concurrent Collections)
当多个线程需要安全地读写共享数据时,并发集合是你的首选,它避免了在简单场景下手动使用锁的复杂性。
- 为何需要:传统的
List<T>或Dictionary<TKey, TValue>在多线程同时读写时会很快崩溃。并发集合内部使用了高效的精巧同步机制,保证了线程安全。 - 常见类型:
ConcurrentQueue<T>:先进先出队列,适用于生产者-消费者模式。ConcurrentDictionary<TKey, TValue>:线程安全的字典,适用于共享缓存。ConcurrentBag<T>:无序集合,适用于对象池等场景。
- 示例:
// 使用 ConcurrentQueue 实现生产者-消费者 private readonly ConcurrentQueue<WorkItem> _workQueue = new ConcurrentQueue<WorkItem>(); // 生产者 public void EnqueueWork(WorkItem item) { _workQueue.Enqueue(item); } // 消费者(通常运行在后台线程) public async Task ProcessWorkAsync(CancellationToken ct) { while (!ct.IsCancellationRequested) { if (_workQueue.TryDequeue(out WorkItem item)) { await ProcessItemAsync(item); } else { await Task.Delay(100, ct); // 短暂休息 } } }
🧰 实战:应对并发挑战的策略
并发编程总会伴随一些挑战,下面是一些常见问题及应对方法:
-
竞态条件 (Race Conditions)
- 问题:多个线程以不确定的顺序访问和操作同一数据,导致结果依赖于执行的时序。
- 解决方案:
- 锁 (
lock):最直接的方式,但要注意粒度,锁得太死会影响性能。 - 并发集合:如前所述,是共享数据访问的优选。
- 不可变类型:使用
System.Collections.Immutable命名空间下的集合,每次“修改”都返回一个新集合,从根本上避免竞态。
- 锁 (
-
死锁 (Deadlocks)
- 问题:两个或多个线程相互等待对方占用的资源,导致所有线程都无法继续执行。
- 解决方案:
- 按固定顺序获取锁:确保所有线程请求锁的顺序是一致的。
- 设置超时:使用
Monitor.TryEnter或SemaphoreSlim.WaitAsync(TimeSpan)并指定超时时间,避免无限期等待。 - 避免嵌套锁:尽量减少锁的嵌套层次。
-
资源管理与性能
- 问题:过度创建线程或任务、不当的阻塞操作(如在异步代码中用
Result或Wait())会导致线程池饥饿,系统性能急剧下降。 - 解决方案:
- 使用
SemaphoreSlim控制并发度:特别是在处理大量突发任务时,限制同时执行的任务数量,保护系统免遭洪水般的请求冲垮。// 使用 SemaphoreSlim 限制最大并发数 private static readonly SemaphoreSlim _semaphore = new SemaphoreSlim(10); // 限制10并发 public async Task ProcessManyItemsAsync(List<Item> items) { var tasks = items.Select(async item => { await _semaphore.WaitAsync(); try { await ProcessItemAsync(item); } finally { _semaphore.Release(); } }); await Task.WhenAll(tasks); } - 区分CPU密集和I/O密集:CPU密集型工作适合用
Parallel或Task.Run分配到多核;I/O密集型工作则应用async/await,在等待时释放线程。 - 始终处理任务异常:使用
await或Task.WaitAll()来传播和处理异常,防止异常被默默吞掉。
- 使用
- 问题:过度创建线程或任务、不当的阻塞操作(如在异步代码中用
📊 并发编程技术选型指南
面对不同的场景,如何选择合适的技术?下面的流程图可以帮你做出决策:
flowchart TD
A[开始: 需要并发吗?] --> B{任务类型是什么?}
B --> C[I/O密集型<br>如: Web请求、数据库操作、文件读写]
B --> D[CPU密集型<br>如: 图像处理、复杂计算、大数据分析]
B --> E[多个独立且耗时操作<br>需要简单触发并行]
B --> F[多线程共享数据<br>如: 缓存、队列、状态字典]
C --> G[使用 async/await<br>高吞吐, 避免阻塞]
D --> H{数据并行?}
H --> I[是, 处理集合<br>使用 Parallel.For/ForEach 或 PLINQ]
H --> J[否, 异构任务<br>使用 Task.Run 启动多个任务]
E --> K[使用 Task.WhenAll<br>等待所有独立任务完成]
F --> L[使用并发集合<br>如: ConcurrentDictionary, ConcurrentQueue]
G --> M[完成]
I --> M
J --> M
K --> M
L --> M
💎 总结
C# 提供了丰富且强大的并发编程工具集。关键在于理解不同技术的适用场景:
- 追求响应和吞吐,
async/await是你的基石。 - 榨干CPU性能,TPL (
Parallel,PLINQ,Task) 是你的利器。 - 安全共享数据,并发集合 是你的护甲。
- 控制并发洪流,
SemaphoreSlim是你的闸门。
更多推荐



所有评论(0)