01. 并发编程全景图:为什么你的代码又慢又卡?
从一个真实的故事开始:
你刚写完一个 ASP.NET Core API,当地测试飞快。摆设上线后,10 个并发用户就能把服务器 CPU 打满,相应时间从 100ms 飙到 5 秒。你懵了:代码没标题啊,为什么性能这么差?
标题的根源,极大概率就藏在并发、并行、异步这三个概念里。搞懂它们,你的代码性能能提拔 10-100 倍。
💡 配套代码:本章全部代码示例都可以在 Overview 项目中运行。
📁 项目结构:
- ConcurrencyDemo.cs - 并发示例(做饭场景)
- ParallelDemo.cs - 并行示例(多核盘算)
- AsyncDemo.cs - 异步示例(模仿下载)
- TaskTypeDemo.cs - 使命范例辨认
- CommonMistakesDemo.cs - 常见误区
🤔 第一个标题:为什么这三个概念这么容易肴杂?
在讨论详细技能之前,我们先搞清晰一个根本标题:为什么并发、并行、异步这么容易肴杂?
答案是:它们都在形貌"同时做多件事",但角度完全差别。
想象你在看一场足球比赛:
- 并发(Concurrency):同一台摄像机在快速切换差别的视角(球员、锻练、观众),你感觉是"同时"看到了全部画面,但现实上是快速切换
- 并行(Parallelism):有 10 个摄像机真正同时拍摄差别的角度
- 异步(Asynchronous):你预约了比赛录像,不消不停盯着电视等,体系会在录制完成后关照你
这三个概念的共同点是都在处理惩罚"多使命",区别在于:
- 并发关注逻辑结构(怎么构造代码)
- 并行关注物理实行(用几个 CPU 核心)
- 异步关注期待方式(壅闭还优劣壅闭)
📌 概念深度分析:不但是界说,更要明白本质
1.1 并发(Concurrency):步伐员的头脑方式
官方界说听起来总是很抽象。让我换个方式说:
并发是你构造代码的方式,让步伐可以或许"处理惩罚"多个使命,而不管这些使命是不是真的同时实行。
为什么必要并发?
现实天下本身就是并发的:
- 你的 Web 服务器要同时处理惩罚 1000 个哀求
- 你的桌面步伐要同时相应用户点击、更新界面、下载文件
- 你的游戏要同时处理惩罚物理盘算、AI、渲染、音效
假如用单线程串行处理惩罚,用户体验会瓦解。
并发的本质:使命切换
关键洞察:单核 CPU 一次只能实行一条指令,但为什么你感觉电脑在"同时"运行 100 个步伐?
答案是时间片轮转:- 时间轴 →
- [任务A][任务B][任务C][任务A][任务B][任务C]...
- 10ns 10ns 10ns 10ns 10ns 10ns
复制代码 切换得富足快,人类就感觉不出来了(人眼辨认延长约 100ms)。
代码示例:并发做饭
这是一个经典的并发场景。留意观察线程 ID:- // 来自 ConcurrencyDemo.cs
- private static async Task ConcurrentCookingAsync()
- {
- Console.WriteLine("开始做饭(并发模式)");
- // 启动三个异步任务
- var task1 = StirFryAsync(); // 炒菜
- var task2 = MakeSoupAsync(); // 煮汤
- var task3 = SteamRiceAsync(); // 蒸米饭
- // 等待所有任务完成
- await Task.WhenAll(task1, task2, task3);
- Console.WriteLine("所有菜都做好了!");
- }
复制代码 运行后你会发现:全部使命大概都在同一个线程上完成!这就是并发的魔力。
💡 运行示例:dotnet run --project Overview 观察线程 ID
深入思考:并发 ≠ 快
紧张认知:并发不是为了"快",而是为了不浪费时间。
做饭时,炒菜必要等油热(I/O 期待),这段时间你可以去切菜(使命切换)。并发让你充实利用期待时间,而不是傻站着。
1.2 并行(Parallelism):硬件的暴力美学
假如说并发是"奇妙地切换",那并行就是"真刀真枪地同时干"。
并行的硬件底子
当代 CPU 都是多核的(4 核、8 核、16 核)。每个核心都是一个独立的盘算单元,能真正同时实行指令。- CPU 核心 1: [计算质数] [计算质数] [计算质数]...
- CPU 核心 2: [计算质数] [计算质数] [计算质数]...
- CPU 核心 3: [计算质数] [计算质数] [计算质数]...
- CPU 核心 4: [计算质数] [计算质数] [计算质数]...
复制代码 什么时间必要并行?
只有一种情况:CPU 麋集型使命。
什么是 CPU 麋集型?就是不必要期待外部资源,纯靠 CPU 盘算的使命:
- 图像处理惩罚(每个像素都要盘算)
- 视频编码(海量数据压缩)
- 科学盘算(模仿、求解方程)
- 大数据分析(筛选、聚合)
代码示例:并行盘算质数
- // 来自 ParallelDemo.cs
- private static void ParallelProcessing()
- {
- Console.WriteLine($"CPU 核心数: {Environment.ProcessorCount}");
- Console.WriteLine("开始并行计算阶乘...");
- var numbers = Enumerable.Range(1, 8).ToArray();
- // 使用 Parallel.ForEach 并行处理
- Parallel.ForEach(numbers, number =>
- {
- var threadId = Environment.CurrentManagedThreadId;
- var result = ComputeFactorial(number);
- Console.WriteLine($" [Thread {threadId}] {number}! = {result}");
- });
- }
复制代码 运行后你会看到:多个差别的线程 ID,它们在真正同时盘算!
💡 运行示例:观察线程 ID 的厘革,明白"真正同时"的寄义
并行的陷阱:Amdahl 定律
暴虐的现实:并行不是 4 核就快 4 倍。
缘故原由有三:
- 线程创建开销:创建和烧毁线程必要时间
- 上下文切换:CPU 在线程间切换必要生存/规复状态
- 数据同步:多线程访问共享数据必要加锁(背面章节详解)
现实加速比通常是 2.5-3.5 倍,已经很不错了。
1.3 异步(Asynchronous):不傻等的艺术
这是最容易被误解的概念,也是当代 .NET 开辟的核心。
为什么必要异步?
想象你在餐厅点餐:
同步方式(壅闭):- 你:我要一份牛排
- 服务员:好的(站在厨房门口等 20 分钟)
- 你:……(也在桌子旁干等)
- [20 分钟后]
- 服务员:您的牛排好了
复制代码 异步方式(非壅闭):- 你:我要一份牛排
- 服务员:好的,请稍等,牛排好了我叫您(转身去服务其他客人)
- 你:……(可以刷手机、聊天)
- [20 分钟后]
- 服务员:先生,您的牛排好了
复制代码 异步的核心:在期待期间,去做其他事变。
异步的硬件底子:I/O 完成端口
许多人不知道,异步操纵在期待期间不占用线程!
当你调用 await httpClient.GetAsync() 时:
- 发起网络哀求(占用线程,非常快,几微秒)
- 线程立刻开释,行止理惩罚其他哀求
- 期待网络相应(不占用线程,这是最耗时的阶段)
- 网卡收到数据后,触发硬件停止
- 操纵体系关照 .NET 运行时
- .NET 从线程池取一个线程继承实行后续代码
关键点:在步调 3(期待相应)期间,没有线程在傻等!
代码示例:异步下载
- // 来自 AsyncDemo.cs
- private static async Task SimulateDownloadAsync(string fileName, int delayMs)
- {
- var startThread = Environment.CurrentManagedThreadId;
- Console.WriteLine($" [Thread {startThread}] 开始下载 {fileName}...");
- // 模拟异步 I/O 操作(等待期间线程被释放)
- await Task.Delay(delayMs);
- var endThread = Environment.CurrentManagedThreadId;
- Console.WriteLine($" [Thread {endThread}] {fileName} 下载完成 ✓");
- // 注意:控制台应用中,await 后可能在同一线程恢复(线程池优化)
- // 在 ASP.NET Core 中,通常会在不同线程恢复
- if (startThread != endThread)
- {
- Console.WriteLine($" → 线程切换:{startThread} → {endThread}");
- }
- else
- {
- Console.WriteLine($" → 线程复用:线程池优化,复用了 Thread {startThread}");
- }
- }
复制代码 运行后你会发现:
- 控制台应用:大概在同一线程完成(线程池优化)
- ASP.NET Core:通常在差别线程规复(有 SynchronizationContext)
- 关键点:无论是否切换线程,期待期间线程都被开释了!
💡 运行示例:观察线程举动
异步的威力:ASP.NET Core 案例
假设你有 100 个线程池线程(默认值),每个哀求必要调用数据库(耗时 50ms):
同步方式:
- 100 个线程同时处理惩罚 100 个哀求
- 每个线程壅闭 50ms 期待数据库
- 第 101 个哀求被拒绝(没有空闲线程)
异步方式:
- 100 个线程发起 100 个数据库哀求,立刻开释
- 这 100 个线程可以继承处理惩罚新的哀求
- 理论上可以同时处理惩罚数千个哀求
性能提拔:10-100 倍!
1.4 三者关系:一个同一的视角
核心洞察:
- 并发是概念,并行和异步是实现方式
- 并行办理盘算瓶颈,异步办理期待浪费
- 它们可以组合利用(比如并行下载 100 个文件)
🔧 .NET 并发技能栈:为什么计划成如许?
许多文章只告诉你"有哪些工具",但不告诉你"为什么要有这些工具"。让我们换个角度。
2.1 演进史:从 Thread 到 async/await
阶段 1:原始期间 - Thread(.NET 1.0-3.5)
- // 2002 年,你是这样写并发代码的
- var thread = new Thread(() =>
- {
- // 下载文件
- var data = DownloadFile(url);
- });
- thread.Start();
- thread.Join(); // 阻塞等待
复制代码 标题:
- 创建线程开销大(1MB+ 栈空间)
- 手动管理生命周期(忘记 Join 导致内存走漏)
- 1000 个并发 = 1000 个线程 = 1GB+ 内存
阶段 2:线程池期间 - ThreadPool(.NET 2.0+)
- // 2005 年,微软引入线程池
- ThreadPool.QueueUserWorkItem(_ =>
- {
- var data = DownloadFile(url);
- });
复制代码 改进:
标题:
- 回调地狱(Callback Hell)
- 错误处理惩罚复杂
- 无法获取返回值
阶段 3:使命期间 - Task(.NET 4.0)
- // 2010 年,Task 横空出世
- var task = Task.Run(() => DownloadFile(url));
- var data = task.Result; // 可以获取返回值了!
复制代码 改进:
- 同一的异步模子
- 支持组合(Task.WhenAll)
- 非常传播机制
标题:
阶段 4:当代异步 - async/await(.NET 4.5+)
- // 2012 年,async/await 改变世界
- var data = await DownloadFileAsync(url);
- // 看起来像同步代码,实际是异步执行!
复制代码 革命性改进:
- 同步风格的异步代码(编译器状态机)
- 主动非常传播
- 完善的组合性
这就是为什么如今保举 async/await!
2.2 技能栈全景:每一层的存在意义
关键洞察:
- 越往上越"业务化",越往下越"体系化"
- 大多数开辟者只必要关注异步编程层和并行编程层
- 同步原语层是"须要之恶"(后续章节详解)
🎯 场景辨认:怎样做出准确的技能选择?
这是最实用的部门。许多开辟者不是不知道工具,而是不知道什么时间用哪个工具。
3.1 核心判定标准:使命在等什么?
一个简朴的标题就能决定齐备:你的代码在等什么?- 任务在等什么?
- │
- ├─ 等 CPU 计算 ────→ CPU 密集型 ────→ 使用并行(Parallel/PLINQ)
- │
- │
- ├─ 等 I/O 完成 ────→ I/O 密集型 ────→ 使用异步(async/await)
- │
- │
- └─ 两者都有 ───────→ 混合型 ────────→ 组合使用
复制代码 3.2 CPU 麋集型:怎样辨认?
特性(满意恣意一条):
- ✅ CPU 利用率 > 70%
- ✅ 代码里有大量循环、递归、数学运算
- ✅ 实行时间与 CPU 主频成反比
典范场景:- // ❌ 错误:用异步处理 CPU 密集任务
- public async Task<int[]> FindPrimesAsync(int max)
- {
- return await Task.Run(() => // 这里的 Task.Run 是对的!
- {
- return Enumerable.Range(2, max)
- .Where(IsPrime) // CPU 密集计算
- .ToArray();
- });
- }
复制代码 准确做法(来自 TaskTypeDemo.cs):- // ✅ 正确:使用 PLINQ
- private static void DemonstrateCpuBoundTask()
- {
- var data = Enumerable.Range(1, 1_000_000).ToArray();
- // 使用 PLINQ 并行处理
- var primes = data
- .AsParallel() // 魔法在这里!
- .Where(IsPrime)
- .ToArray();
- Console.WriteLine($"找到 {primes.Length} 个质数");
- }
复制代码 性能提拔:4 核 CPU 上约 2.5-3.5 倍
💡 运行示例:dotnet run --project Overview 观察耗时
3.3 I/O 麋集型:最容易踩坑的地方
特性(满意恣意一条):
- ✅ CPU 利用率 < 30%
- ✅ 代码在等网络、磁盘、数据库
- ✅ 实行时间与网络延长成正比
常见错误(90% 的性能标题都是这个):- // ❌ 错误:用 Task.Run 包装 I/O 操作
- public async Task<string> GetDataAsync()
- {
- return await Task.Run(async () => // ❌ 画蛇添足!
- {
- using var client = new HttpClient();
- return await client.GetStringAsync(url); // I/O 操作
- });
- }
复制代码 为什么错?
- HttpClient.GetStringAsync 已经是异步的(不占用线程)
- Task.Run 额外占用一个线程池线程
- 这个线程在干什么?傻等网络相应!
准确做法:- // ✅ 正确:直接 await
- public async Task<string> GetDataAsync()
- {
- using var client = new HttpClient();
- return await client.GetStringAsync(url);
- // 等待期间,线程被释放去处理其他请求
- }
复制代码💡 运行示例:检察 CommonMistakesDemo.cs 的性能对比
性能影响:在高并发下,吞吐量差异可达 10-100 倍!
这个标题非常的典范,大部门人会在这里踩坑,背面的章节会偏重解说缘故原由
3.4 混淆型使命:组合的艺术
真实天下的使命每每是混淆的。关键是辨认每个步调的范例。
案例:批量下载并处理惩罚图片- // 来自 TaskTypeDemo.cs(改进版)
- public async Task ProcessImagesAsync(string[] urls)
- {
- // 步骤 1:I/O 密集 - 并发下载(异步)
- var downloadTasks = urls.Select(url => DownloadImageAsync(url));
- var images = await Task.WhenAll(downloadTasks);
-
- // 步骤 2:CPU 密集 - 并行处理(Parallel)
- var processed = new ConcurrentBag<Image>();
- Parallel.ForEach(images, image =>
- {
- var result = ApplyFilters(image); // CPU 密集
- processed.Add(result);
- });
-
- // 步骤 3:I/O 密集 - 并发保存(异步)
- var saveTasks = processed.Select(img => SaveImageAsync(img));
- await Task.WhenAll(saveTasks);
- }
复制代码 关键洞察:
- I/O 步调用 async/await(期待期间不占用线程)
- CPU 步调用 Parallel(充实利用多核)
- 不要肴杂:I/O 不必要 Task.Run
💡 实战案例:从 100ms 到 10ms 的优化之旅
让我分享一个真实的优化案例,展示怎样辨认和办理性能标题。
案例配景
一个电商 API,用户查询订单详情:
- 查询数据库(50ms)
- 调用物流 API(100ms)
- 调用付出 API(80ms)
初始性能:总耗时 230ms
第一版:同步壅闭(新手代码)
- // ❌ 性能:230ms,吞吐量:100 QPS
- public IActionResult GetOrder(int orderId)
- {
- // 阻塞等待数据库
- var order = _db.Orders.FirstOrDefault(o => o.Id == orderId);
-
- // 阻塞等待物流 API
- var logistics = _logisticsClient.GetAsync(order.TrackingNo).Result;
-
- // 阻塞等待支付 API
- var payment = _paymentClient.GetAsync(order.PaymentId).Result;
-
- return Ok(new { order, logistics, payment });
- }
复制代码 标题:3 个线程在干什么?傻等 I/O!
第二版:异步串行(低级优化)
- // ⚠️ 性能:230ms,吞吐量:10000 QPS
- public async Task<IActionResult> GetOrderAsync(int orderId)
- {
- // 异步查询数据库
- var order = await _db.Orders.FirstOrDefaultAsync(o => o.Id == orderId);
-
- // 异步调用物流 API
- var logistics = await _logisticsClient.GetAsync(order.TrackingNo);
-
- // 异步调用支付 API
- var payment = await _paymentClient.GetAsync(order.PaymentId);
-
- return Ok(new { order, logistics, payment });
- }
复制代码 改进:
- ✅ 吞吐量提拔 100 倍(线程不再壅闭)
- ❌ 但相应时间没变(仍然是串行)
第三版:异步并发(高级优化)
- // ✅ 性能:100ms,吞吐量:10000 QPS
- public async Task<IActionResult> GetOrderAsync(int orderId)
- {
- // 异步查询数据库
- var order = await _db.Orders.FirstOrDefaultAsync(o => o.Id == orderId);
-
- // 并发调用两个 API(它们互不依赖)
- var logisticsTask = _logisticsClient.GetAsync(order.TrackingNo);
- var paymentTask = _paymentClient.GetAsync(order.PaymentId);
-
- // 等待两个任务都完成
- await Task.WhenAll(logisticsTask, paymentTask);
-
- return Ok(new
- {
- order,
- logistics = logisticsTask.Result,
- payment = paymentTask.Result
- });
- }
复制代码 最闭幕果:
- ✅ 相应时间:230ms → 100ms(提拔 2.3 倍)
- ✅ 吞吐量:100 QPS → 10000 QPS(提拔 100 倍)
关键洞察:
- 物流和付出 API 可以并发调用(它们互不依靠)
- 100ms 是最慢的谁人 API 的耗时
⚠️ 常见误区:为什么 90% 的开辟者会犯这些错?
误区 1:"async 就是多线程"
错误认知:加了 async 关键字就会创建新线程。
原形:async 只是编译器的语法糖,天生一个状态机。
证实:- public async Task TestAsync()
- {
- Console.WriteLine($"Before: {Thread.CurrentThread.ManagedThreadId}");
- await Task.Delay(1000); // 异步等待
- Console.WriteLine($"After: {Thread.CurrentThread.ManagedThreadId}");
- }
复制代码 运行效果:线程 ID 大概类似!
为什么会有这个误区?
- 由于 Task.Run 确实会用线程池线程
- 但 await 本身不会创建线程
误区 2:"Task.Run 能提拔性能"
错误认知:把任何操纵包装在 Task.Run 里就能变快。
原形(来自 CommonMistakesDemo.cs):- // ❌ 错误:浪费线程
- var data = await Task.Run(async () =>
- {
- await Task.Delay(500); // I/O 操作
- return "Data";
- });
- // ✅ 正确:直接 await
- await Task.Delay(500);
- var data = "Data";
复制代码 为什么错?
- Task.Delay 已经是异步的(不占用线程)
- Task.Run 额外占用一个线程池线程
- 这个线程在 await Task.Delay 期间照旧被开释了
- 效果:多余的线程调治开销,性能反而低落
准确利用 Task.Run:仅用于 CPU 麋集型使命!
误区 3:"Task 就是线程"
错误认知:创建 1000 个 Task 就会创建 1000 个线程。
原形:
- I/O 麋集型 Task:在期待期间不占用线程(利用 I/O 完成端口)
- CPU 麋集型 Task:利用线程池线程(通常 < 100 个)
验证代码:- // 创建 10000 个 I/O 密集型 Task
- var tasks = Enumerable.Range(1, 10000)
- .Select(_ => Task.Delay(5000))
- .ToArray();
- await Task.WhenAll(tasks);
- // 线程池线程数:几乎没增加!
复制代码 为什么会有这个误区?
- 由于在其他语言(如 Go、Erlang)中,一个使命确实对应一个"协程"
- 但 .NET 的 Task 是异步操纵的抽象,不便是线程
🎯 速查表:30 秒做出准确选择
场景特性保举技能制止性能提拔Web API 调用期待网络相应async/await + HttpClientTask.Run10-100x数据库查询期待数据库相应async/await + EF Core Async.Result / .Wait()10-100x文件读写期待磁盘 I/Oasync/await + Stream同步 I/O5-20x图像处理惩罚CPU 盘算Parallel.ForEachasync/await2.5-3.5x视频编码CPU 盘算Parallel + Task.Run单线程2.5-3.5x数据分析CPU 盘算PLINQ平凡 LINQ2.5-3.5x批量下载I/O 麋集Task.WhenAll + async/await串行下载文件数倍混淆使命I/O + CPU组合利用全用异步或全用并行5-20x记着一个原则:
- 等 I/O → async/await
- 等 CPU → Parallel/PLINQ
📌 本章小结:从狐疑到清晰
核心洞察
- 并发、并行、异步的本质:
- 并发 = 构造代码的方式(逻辑层面)
- 并行 = 利用多核硬件(物理层面)
- 异步 = 不浪费期待时间(实行模式)
- 技能选择的黄金法则:
- I/O 麋集 → async/await(开释线程)
- CPU 麋集 → Parallel(利用多核)
- 混淆型 → 组合利用
- 常见误区的根源:
- Task ≠ 线程
- async ≠ 多线程
- Task.Run ≠ 性能提拔
- 性能优化的原形:
💭 思考题
标题 1:为什么异步能提拔吞吐量,但不肯定能低落相应时间?
💡 答案分析
吞吐量 vs 相应时间:
- 吞吐量(Throughput)= 单元时间处理惩罚的哀求数
- 相应时间(Latency)= 单个哀求的完成时间
异步的核心是开释线程,让一个线程能处理惩罚更多哀求:
- 同步:100 个线程 → 处理惩罚 100 个哀求
- 异步:100 个线程 → 处理惩罚 10000 个哀求(线程被复用)
但单个哀求的耗时(如网络延长 100ms)不会变。
要低落相应时间,必要:
- 并发实行(Task.WhenAll)
- 缓存
- 更快的网络/数据库
标题 2:下面的代码有什么标题?
- public async Task ProcessDataAsync()
- {
- var data = await DownloadDataAsync(); // I/O
- var result = await Task.Run(() =>
- {
- return data.Select(x => x * 2).ToList(); // CPU?
- });
- }
复制代码💡 答案分析
标题:Select 操纵不是 CPU 麋集型,不必要 Task.Run。
改进:- public async Task ProcessDataAsync()
- {
- var data = await DownloadDataAsync(); // I/O
- var result = data.Select(x => x * 2).ToList(); // 同步即可
- }
复制代码 什么时间必要 Task.Run?
- 循环次数 > 10000
- 单次循环耗时 > 1ms
- 总耗时 > 100ms
简朴的 LINQ 操纵不必要!
标题 3:为什么 ASP.NET Core 猛烈发起全部利用异步?
💡 答案分析
Web 应用的特点:
- 99% 的时间在等 I/O(数据库、缓存、外部 API)
- 哀求数量大(大概同时有数千个哀求)
同步的标题:
- 100 个线程 → 只能处理惩罚 100 个并发哀求
- 第 101 个哀求被拒绝(HTTP 503)
异步的上风:
- 100 个线程 → 可以处理惩罚 10000+ 个并发哀求
- 吞吐量提拔 100 倍
现实数据(微软官方测试):
- 同步:1000 并发 → CPU 100%,相应时间 5s
- 异步:1000 并发 → CPU 20%,相应时间 100ms
🚀 下一步
在下一章《02. 线程的底层:Thread、ThreadPool 与 Task 的关系》中,我们将:
- 用代码实行验证 Thread 的真实本钱(内存、上下文切换)
- 观察 ThreadPool 的工作偷取算法(为什么比你手动管理更高效)
- 明白 Task 如安在底层调治(状态机、线程复用)
- 回复为什么当代 .NET 保举 Task 而不是 Thread
预报一个震撼的实行:- // 创建 10000 个 Thread:内存占用 10GB+,系统崩溃
- // 创建 10000 个 Task:内存占用 < 100MB,完美运行
复制代码 示例代码堆栈:https://github.com/Naughtyhusky/csharp-concurrency-cookbook
有标题?接待留言讨论!
免责声明:如果侵犯了您的权益,请联系站长及时删除侵权内容,谢谢合作!qidao123.com:ToB企服之家,中国第一个企服评测及软件市场,开放入驻,技术点评得现金. |