【.NET并发编程 - 01】并发编程全景图
<h1 id="01-并发编程全景图为什么你的代码又慢又卡">01. 并发编程全景图:为什么你的代码又慢又卡?</h1><blockquote>
<p><strong>从一个真实的故事开始</strong>:<br>
你刚写完一个 ASP.NET Core API,本地测试飞快。部署上线后,10 个并发用户就能把服务器 CPU 打满,响应时间从 100ms 飙到 5 秒。你懵了:代码没问题啊,为什么性能这么差?</p>
<p>问题的根源,极大概率就藏在<strong>并发、并行、异步</strong>这三个概念里。搞懂它们,你的代码性能能提升 <strong>10-100 倍</strong>。</p>
</blockquote>
<blockquote>
<p>💡 <strong>配套代码</strong>:本章所有代码示例都可以在 <code>Overview</code> 项目中运行。<br>
📁 <strong>项目结构</strong>:</p>
<ul>
<li><code>ConcurrencyDemo.cs</code> - 并发示例(做饭场景)</li>
<li><code>ParallelDemo.cs</code> - 并行示例(多核计算)</li>
<li><code>AsyncDemo.cs</code> - 异步示例(模拟下载)</li>
<li><code>TaskTypeDemo.cs</code> - 任务类型识别</li>
<li><code>CommonMistakesDemo.cs</code> - 常见误区</li>
</ul>
</blockquote>
<hr>
<h2 id="-第一个问题为什么这三个概念这么容易混淆">🤔 第一个问题:为什么这三个概念这么容易混淆?</h2>
<p>在讨论具体技术之前,我们先搞清楚一个根本问题:<strong>为什么并发、并行、异步这么容易混淆?</strong></p>
<p>答案是:<strong>它们都在描述"同时做多件事",但角度完全不同</strong>。</p>
<p>想象你在看一场足球比赛:</p>
<ul>
<li><strong>并发(Concurrency)</strong>:同一台摄像机在快速切换不同的视角(球员、教练、观众),你感觉是"同时"看到了所有画面,但实际上是快速切换</li>
<li><strong>并行(Parallelism)</strong>:有 10 个摄像机真正同时拍摄不同的角度</li>
<li><strong>异步(Asynchronous)</strong>:你预约了比赛录像,不用一直盯着电视等,系统会在录制完成后通知你</li>
</ul>
<p>这三个概念的<strong>共同点</strong>是都在处理"多任务",<strong>区别</strong>在于:</p>
<ul>
<li>并发关注<strong>逻辑结构</strong>(怎么组织代码)</li>
<li>并行关注<strong>物理执行</strong>(用几个 CPU 核心)</li>
<li>异步关注<strong>等待方式</strong>(阻塞还是非阻塞)</li>
</ul>
<hr>
<h2 id="-概念深度剖析不只是定义更要理解本质">📌 概念深度剖析:不只是定义,更要理解本质</h2>
<h3 id="11-并发concurrency程序员的思维方式">1.1 并发(Concurrency):程序员的思维方式</h3>
<p><strong>官方定义</strong>听起来总是很抽象。让我换个方式说:</p>
<p><strong>并发是你组织代码的方式,让程序能够"处理"多个任务,而不管这些任务是不是真的同时执行。</strong></p>
<h4 id="为什么需要并发">为什么需要并发?</h4>
<p>现实世界本身就是并发的:</p>
<ul>
<li>你的 Web 服务器要同时处理 1000 个请求</li>
<li>你的桌面程序要同时响应用户点击、更新界面、下载文件</li>
<li>你的游戏要同时处理物理计算、AI、渲染、音效</li>
</ul>
<p>如果用单线程串行处理,用户体验会崩溃。</p>
<h4 id="并发的本质任务切换">并发的本质:任务切换</h4>
<p><strong>关键洞察</strong>:单核 CPU 一次只能执行一条指令,但为什么你感觉电脑在"同时"运行 100 个程序?</p>
<p>答案是<strong>时间片轮转</strong>:</p>
<pre><code>时间轴 →
[任务A][任务B][任务C][任务A][任务B][任务C]...
10ns 10ns10ns10ns10ns10ns
</code></pre>
<p>切换得足够快,人类就感觉不出来了(人眼识别延迟约 100ms)。</p>
<h4 id="代码示例并发做饭">代码示例:并发做饭</h4>
<p>这是一个经典的并发场景。注意观察<strong>线程 ID</strong>:</p>
<pre><code class="language-csharp">// 来自 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("所有菜都做好了!");
}
</code></pre>
<p><strong>运行后你会发现</strong>:所有任务可能都在<strong>同一个线程</strong>上完成!这就是并发的魔力。</p>
<blockquote>
<p>💡 <strong>运行示例</strong>:<code>dotnet run --project Overview</code> 观察线程 ID</p>
</blockquote>
<h4 id="深入思考并发--快">深入思考:并发 ≠ 快</h4>
<p><strong>重要认知</strong>:并发不是为了"快",而是为了<strong>不浪费时间</strong>。</p>
<p>做饭时,炒菜需要等油热(I/O 等待),这段时间你可以去切菜(任务切换)。并发让你充分利用<strong>等待时间</strong>,而不是傻站着。</p>
<hr>
<h3 id="12-并行parallelism硬件的暴力美学">1.2 并行(Parallelism):硬件的暴力美学</h3>
<p>如果说并发是"巧妙地切换",那并行就是"真刀真枪地同时干"。</p>
<h4 id="并行的硬件基础">并行的硬件基础</h4>
<p>现代 CPU 都是多核的(4 核、8 核、16 核)。每个核心都是一个独立的计算单元,能<strong>真正同时</strong>执行指令。</p>
<pre><code>CPU 核心 1: [计算质数] [计算质数] [计算质数]...
CPU 核心 2: [计算质数] [计算质数] [计算质数]...
CPU 核心 3: [计算质数] [计算质数] [计算质数]...
CPU 核心 4: [计算质数] [计算质数] [计算质数]...
</code></pre>
<h4 id="什么时候需要并行">什么时候需要并行?</h4>
<p><strong>只有一种情况:CPU 密集型任务。</strong></p>
<p>什么是 CPU 密集型?就是<strong>不需要等待外部资源,纯靠 CPU 计算的任务</strong>:</p>
<ul>
<li>图像处理(每个像素都要计算)</li>
<li>视频编码(海量数据压缩)</li>
<li>科学计算(模拟、求解方程)</li>
<li>大数据分析(筛选、聚合)</li>
</ul>
<h4 id="代码示例并行计算质数">代码示例:并行计算质数</h4>
<pre><code class="language-csharp">// 来自 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($" {number}! = {result}");
});
}
</code></pre>
<p><strong>运行后你会看到</strong>:多个不同的线程 ID,它们在<strong>真正同时</strong>计算!</p>
<blockquote>
<p>💡 <strong>运行示例</strong>:观察线程 ID 的变化,理解"真正同时"的含义</p>
</blockquote>
<h4 id="并行的陷阱amdahl-定律">并行的陷阱:Amdahl 定律</h4>
<p><strong>残酷的现实</strong>:并行不是 4 核就快 4 倍。</p>
<p>原因有三:</p>
<ol>
<li><strong>线程创建开销</strong>:创建和销毁线程需要时间</li>
<li><strong>上下文切换</strong>:CPU 在线程间切换需要保存/恢复状态</li>
<li><strong>数据同步</strong>:多线程访问共享数据需要加锁(后面章节详解)</li>
</ol>
<p>实际加速比通常是 <strong>2.5-3.5 倍</strong>,已经很不错了。</p>
<hr>
<h3 id="13-异步asynchronous不傻等的艺术">1.3 异步(Asynchronous):不傻等的艺术</h3>
<p>这是<strong>最容易被误解</strong>的概念,也是现代 .NET 开发的核心。</p>
<h4 id="为什么需要异步">为什么需要异步?</h4>
<p>想象你在餐厅点餐:</p>
<p><strong>同步方式(阻塞)</strong>:</p>
<pre><code>你:我要一份牛排
服务员:好的(站在厨房门口等 20 分钟)
你:……(也在桌子旁干等)
服务员:您的牛排好了
</code></pre>
<p><strong>异步方式(非阻塞)</strong>:</p>
<pre><code>你:我要一份牛排
服务员:好的,请稍等,牛排好了我叫您(转身去服务其他客人)
你:……(可以刷手机、聊天)
服务员:先生,您的牛排好了
</code></pre>
<p>异步的核心:<strong>在等待期间,去做其他事情</strong>。</p>
<h4 id="异步的硬件基础io-完成端口">异步的硬件基础:I/O 完成端口</h4>
<p>很多人不知道,异步操作<strong>在等待期间不占用线程</strong>!</p>
<p>当你调用 <code>await httpClient.GetAsync()</code> 时:</p>
<ol>
<li><strong>发起网络请求</strong>(占用线程,非常快,几微秒)</li>
<li>线程<strong>立即释放</strong>,去处理其他请求</li>
<li><strong>等待网络响应</strong>(不占用线程,这是最耗时的阶段)</li>
<li>网卡收到数据后,触发硬件中断</li>
<li>操作系统通知 .NET 运行时</li>
<li>.NET 从线程池取一个线程继续执行后续代码</li>
</ol>
<p><strong>关键点</strong>:在步骤 3(等待响应)期间,没有线程在傻等!</p>
<h4 id="代码示例异步下载">代码示例:异步下载</h4>
<pre><code class="language-csharp">// 来自 AsyncDemo.cs
private static async Task SimulateDownloadAsync(string fileName, int delayMs)
{
var startThread = Environment.CurrentManagedThreadId;
Console.WriteLine($" 开始下载 {fileName}...");
// 模拟异步 I/O 操作(等待期间线程被释放)
await Task.Delay(delayMs);
var endThread = Environment.CurrentManagedThreadId;
Console.WriteLine($" {fileName} 下载完成 ✓");
// 注意:控制台应用中,await 后可能在同一线程恢复(线程池优化)
// 在 ASP.NET Core 中,通常会在不同线程恢复
if (startThread != endThread)
{
Console.WriteLine($"→ 线程切换:{startThread} → {endThread}");
}
else
{
Console.WriteLine($"→ 线程复用:线程池优化,复用了 Thread {startThread}");
}
}
</code></pre>
<p><strong>运行后你会发现</strong>:</p>
<ul>
<li><strong>控制台应用</strong>:可能在<strong>同一线程</strong>完成(线程池优化)</li>
<li><strong>ASP.NET Core</strong>:通常在<strong>不同线程</strong>恢复(有 SynchronizationContext)</li>
<li><strong>关键点</strong>:无论是否切换线程,等待期间线程都被释放了!</li>
</ul>
<blockquote>
<p>💡 <strong>运行示例</strong>:观察线程行为</p>
</blockquote>
<h4 id="异步的威力aspnet-core-案例">异步的威力:ASP.NET Core 案例</h4>
<p>假设你有 100 个线程池线程(默认值),每个请求需要调用数据库(耗时 50ms):</p>
<p><strong>同步方式</strong>:</p>
<ul>
<li>100 个线程同时处理 100 个请求</li>
<li>每个线程阻塞 50ms 等待数据库</li>
<li>第 101 个请求<strong>被拒绝</strong>(没有空闲线程)</li>
</ul>
<p><strong>异步方式</strong>:</p>
<ul>
<li>100 个线程发起 100 个数据库请求,立即释放</li>
<li>这 100 个线程可以继续处理新的请求</li>
<li>理论上可以同时处理<strong>数千个请求</strong></li>
</ul>
<p>性能提升:<strong>10-100 倍</strong>!</p>
<hr>
<h3 id="14-三者关系一个统一的视角">1.4 三者关系:一个统一的视角</h3>
<p><img src="https://img2024.cnblogs.com/blog/1553709/202604/1553709-20260412192601306-1723759705.png"></p>
<p><strong>核心洞察</strong>:</p>
<ul>
<li>并发是<strong>概念</strong>,并行和异步是<strong>实现方式</strong></li>
<li>并行解决<strong>计算瓶颈</strong>,异步解决<strong>等待浪费</strong></li>
<li>它们可以<strong>组合使用</strong>(比如并行下载 100 个文件)</li>
</ul>
<hr>
<h2 id="-net-并发技术栈为什么设计成这样">🔧 .NET 并发技术栈:为什么设计成这样?</h2>
<p>很多文章只告诉你"有哪些工具",但不告诉你"为什么要有这些工具"。让我们换个角度。</p>
<h3 id="21-演进史从-thread-到-asyncawait">2.1 演进史:从 Thread 到 async/await</h3>
<h4 id="阶段-1原始时代---threadnet-10-35">阶段 1:原始时代 - Thread(.NET 1.0-3.5)</h4>
<pre><code class="language-csharp">// 2002 年,你是这样写并发代码的
var thread = new Thread(() =>
{
// 下载文件
var data = DownloadFile(url);
});
thread.Start();
thread.Join(); // 阻塞等待
</code></pre>
<p><strong>问题</strong>:</p>
<ul>
<li>创建线程开销大(1MB+ 栈空间)</li>
<li>手动管理生命周期(忘记 Join 导致内存泄漏)</li>
<li>1000 个并发 = 1000 个线程 = 1GB+ 内存</li>
</ul>
<h4 id="阶段-2线程池时代---threadpoolnet-20">阶段 2:线程池时代 - ThreadPool(.NET 2.0+)</h4>
<pre><code class="language-csharp">// 2005 年,微软引入线程池
ThreadPool.QueueUserWorkItem(_ =>
{
var data = DownloadFile(url);
});
</code></pre>
<p><strong>改进</strong>:</p>
<ul>
<li>复用线程,避免重复创建</li>
<li>自动管理线程数量</li>
</ul>
<p><strong>问题</strong>:</p>
<ul>
<li>回调地狱(Callback Hell)</li>
<li>错误处理复杂</li>
<li>无法获取返回值</li>
</ul>
<h4 id="阶段-3任务时代---tasknet-40">阶段 3:任务时代 - Task(.NET 4.0)</h4>
<pre><code class="language-csharp">// 2010 年,Task 横空出世
var task = Task.Run(() => DownloadFile(url));
var data = task.Result; // 可以获取返回值了!
</code></pre>
<p><strong>改进</strong>:</p>
<ul>
<li>统一的异步模型</li>
<li>支持组合(Task.WhenAll)</li>
<li>异常传播机制</li>
</ul>
<p><strong>问题</strong>:</p>
<ul>
<li>仍然是回调风格</li>
<li>代码可读性差</li>
</ul>
<h4 id="阶段-4现代异步---asyncawaitnet-45">阶段 4:现代异步 - async/await(.NET 4.5+)</h4>
<pre><code class="language-csharp">// 2012 年,async/await 改变世界
var data = await DownloadFileAsync(url);
// 看起来像同步代码,实际是异步执行!
</code></pre>
<p><strong>革命性改进</strong>:</p>
<ul>
<li><strong>同步风格的异步代码</strong>(编译器状态机)</li>
<li>自动异常传播</li>
<li>完美的组合性</li>
</ul>
<p>这就是为什么现在推荐 <code>async/await</code>!</p>
<h3 id="22-技术栈全景每一层的存在意义">2.2 技术栈全景:每一层的存在意义</h3>
<p><img src="https://img2024.cnblogs.com/blog/1553709/202604/1553709-20260412192623352-1287931074.png"></p>
<p><strong>关键洞察</strong>:</p>
<ul>
<li>越往上越"业务化",越往下越"系统化"</li>
<li>大多数开发者只需要关注<strong>异步编程层</strong>和<strong>并行编程层</strong></li>
<li>同步原语层是"必要之恶"(后续章节详解)</li>
</ul>
<hr>
<h2 id="-场景识别如何做出正确的技术选择">🎯 场景识别:如何做出正确的技术选择?</h2>
<p>这是最实用的部分。很多开发者不是不知道工具,而是<strong>不知道什么时候用哪个工具</strong>。</p>
<h3 id="31-核心判断标准任务在等什么">3.1 核心判断标准:任务在等什么?</h3>
<p><strong>一个简单的问题就能决定一切</strong>:你的代码在等什么?</p>
<pre><code>任务在等什么?
│
├─ 等 CPU 计算 ────→ CPU 密集型 ────→ 使用并行(Parallel/PLINQ)
│
│
├─ 等 I/O 完成 ────→ I/O 密集型 ────→ 使用异步(async/await)
│
│
└─ 两者都有 ───────→ 混合型 ────────→ 组合使用
</code></pre>
<h3 id="32-cpu-密集型如何识别">3.2 CPU 密集型:如何识别?</h3>
<p><strong>特征</strong>(满足任意一条):</p>
<ul>
<li>✅ CPU 使用率 > 70%</li>
<li>✅ 代码里有大量循环、递归、数学运算</li>
<li>✅ 执行时间与 CPU 主频成反比</li>
</ul>
<p><strong>典型场景</strong>:</p>
<pre><code class="language-csharp">// ❌ 错误:用异步处理 CPU 密集任务
public async Task<int[]> FindPrimesAsync(int max)
{
return await Task.Run(() =>// 这里的 Task.Run 是对的!
{
return Enumerable.Range(2, max)
.Where(IsPrime)// CPU 密集计算
.ToArray();
});
}
</code></pre>
<p><strong>正确做法</strong>(来自 <code>TaskTypeDemo.cs</code>):</p>
<pre><code class="language-csharp">// ✅ 正确:使用 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} 个质数");
}
</code></pre>
<p><strong>性能提升</strong>:4 核 CPU 上约 <strong>2.5-3.5 倍</strong></p>
<blockquote>
<p>💡 <strong>运行示例</strong>:<code>dotnet run --project Overview</code> 观察耗时</p>
</blockquote>
<h3 id="33-io-密集型最容易踩坑的地方">3.3 I/O 密集型:最容易踩坑的地方</h3>
<p><strong>特征</strong>(满足任意一条):</p>
<ul>
<li>✅ CPU 使用率 < 30%</li>
<li>✅ 代码在等网络、磁盘、数据库</li>
<li>✅ 执行时间与网络延迟成正比</li>
</ul>
<p><strong>常见错误</strong>(90% 的性能问题都是这个):</p>
<pre><code class="language-csharp">// ❌ 错误:用 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 操作
});
}
</code></pre>
<p><strong>为什么错?</strong></p>
<ol>
<li><code>HttpClient.GetStringAsync</code> 已经是异步的(不占用线程)</li>
<li><code>Task.Run</code> 额外占用一个线程池线程</li>
<li>这个线程在干什么?<strong>傻等网络响应</strong>!</li>
</ol>
<p><strong>正确做法</strong>:</p>
<pre><code class="language-csharp">// ✅ 正确:直接 await
public async Task<string> GetDataAsync()
{
using var client = new HttpClient();
return await client.GetStringAsync(url);
// 等待期间,线程被释放去处理其他请求
}
</code></pre>
<blockquote>
<p>💡 <strong>运行示例</strong>:查看 <code>CommonMistakesDemo.cs</code> 的性能对比</p>
</blockquote>
<p><strong>性能影响</strong>:在高并发下,吞吐量差异可达 <strong>10-100 倍</strong>!</p>
<blockquote>
<p>这个问题非常的典型,大部分人会在这里踩坑,后面的章节会着重讲解原因</p>
</blockquote>
<h3 id="34-混合型任务组合的艺术">3.4 混合型任务:组合的艺术</h3>
<p>真实世界的任务往往是混合的。关键是<strong>识别每个步骤的类型</strong>。</p>
<p><strong>案例:批量下载并处理图片</strong></p>
<pre><code class="language-csharp">// 来自 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);
}
</code></pre>
<p><strong>关键洞察</strong>:</p>
<ul>
<li>I/O 步骤用 <code>async/await</code>(<strong>等待期间不占用线程</strong>)</li>
<li>CPU 步骤用 <code>Parallel</code>(充分利用多核)</li>
<li><strong>不要混淆</strong>:I/O 不需要 <code>Task.Run</code></li>
</ul>
<hr>
<h2 id="-实战案例从-100ms-到-10ms-的优化之旅">💡 实战案例:从 100ms 到 10ms 的优化之旅</h2>
<p>让我分享一个真实的优化案例,展示如何识别和解决性能问题。</p>
<h3 id="案例背景">案例背景</h3>
<p>一个电商 API,用户查询订单详情:</p>
<ul>
<li>查询数据库(50ms)</li>
<li>调用物流 API(100ms)</li>
<li>调用支付 API(80ms)</li>
</ul>
<p>初始性能:<strong>总耗时 230ms</strong></p>
<h3 id="第一版同步阻塞新手代码">第一版:同步阻塞(新手代码)</h3>
<pre><code class="language-csharp">// ❌ 性能: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 });
}
</code></pre>
<p><strong>问题</strong>:3 个线程在干什么?<strong>傻等 I/O</strong>!</p>
<h3 id="第二版异步串行初级优化">第二版:异步串行(初级优化)</h3>
<pre><code class="language-csharp">// ⚠️性能: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 });
}
</code></pre>
<p><strong>改进</strong>:</p>
<ul>
<li>✅ 吞吐量提升 <strong>100 倍</strong>(线程不再阻塞)</li>
<li>❌ 但响应时间没变(仍然是串行)</li>
</ul>
<h3 id="第三版异步并发高级优化">第三版:异步并发(高级优化)</h3>
<pre><code class="language-csharp">// ✅ 性能: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
});
}
</code></pre>
<p><strong>最终结果</strong>:</p>
<ul>
<li>✅ 响应时间:230ms → <strong>100ms</strong>(提升 2.3 倍)</li>
<li>✅ 吞吐量:100 QPS → <strong>10000 QPS</strong>(提升 100 倍)</li>
</ul>
<p><strong>关键洞察</strong>:</p>
<ul>
<li>物流和支付 API 可以<strong>并发调用</strong>(它们互不依赖)</li>
<li>100ms 是最慢的那个 API 的耗时</li>
</ul>
<hr>
<h2 id="️-常见误区为什么-90-的开发者会犯这些错">⚠️ 常见误区:为什么 90% 的开发者会犯这些错?</h2>
<h3 id="误区-1async-就是多线程">误区 1:"async 就是多线程"</h3>
<p><strong>错误认知</strong>:加了 <code>async</code> 关键字就会创建新线程。</p>
<p><strong>真相</strong>:<code>async</code> 只是编译器的<strong>语法糖</strong>,生成一个状态机。</p>
<p><strong>证明</strong>:</p>
<pre><code class="language-csharp">public async Task TestAsync()
{
Console.WriteLine($"Before: {Thread.CurrentThread.ManagedThreadId}");
await Task.Delay(1000);// 异步等待
Console.WriteLine($"After: {Thread.CurrentThread.ManagedThreadId}");
}
</code></pre>
<p>运行结果:<strong>线程 ID 可能相同</strong>!</p>
<p><strong>为什么会有这个误区?</strong></p>
<ul>
<li>因为 <code>Task.Run</code> 确实会用线程池线程</li>
<li>但 <code>await</code> 本身不会创建线程</li>
</ul>
<hr>
<h3 id="误区-2taskrun-能提升性能">误区 2:"Task.Run 能提升性能"</h3>
<p><strong>错误认知</strong>:把任何操作包装在 <code>Task.Run</code> 里就能变快。</p>
<p><strong>真相</strong>(来自 <code>CommonMistakesDemo.cs</code>):</p>
<pre><code class="language-csharp">// ❌ 错误:浪费线程
var data = await Task.Run(async () =>
{
await Task.Delay(500);// I/O 操作
return "Data";
});
// ✅ 正确:直接 await
await Task.Delay(500);
var data = "Data";
</code></pre>
<p><strong>为什么错?</strong></p>
<ul>
<li><code>Task.Delay</code> 已经是异步的(不占用线程)</li>
<li><code>Task.Run</code> 额外占用一个线程池线程</li>
<li>这个线程在 <code>await Task.Delay</code> 期间<strong>还是被释放了</strong></li>
<li>结果:多余的线程调度开销,性能反而降低</li>
</ul>
<p><strong>正确使用 Task.Run</strong>:仅用于 CPU 密集型任务!</p>
<hr>
<h3 id="误区-3task-就是线程">误区 3:"Task 就是线程"</h3>
<p><strong>错误认知</strong>:创建 1000 个 Task 就会创建 1000 个线程。</p>
<p><strong>真相</strong>:</p>
<ul>
<li><strong>I/O 密集型 Task</strong>:在等待期间不占用线程(使用 I/O 完成端口)</li>
<li><strong>CPU 密集型 Task</strong>:使用线程池线程(通常 < 100 个)</li>
</ul>
<p><strong>验证代码</strong>:</p>
<pre><code class="language-csharp">// 创建 10000 个 I/O 密集型 Task
var tasks = Enumerable.Range(1, 10000)
.Select(_ => Task.Delay(5000))
.ToArray();
await Task.WhenAll(tasks);
// 线程池线程数:几乎没增加!
</code></pre>
<p><strong>为什么会有这个误区?</strong></p>
<ul>
<li>因为在其他语言(如 Go、Erlang)中,一个任务确实对应一个"协程"</li>
<li>但 .NET 的 Task 是<strong>异步操作的抽象</strong>,不等于线程</li>
</ul>
<hr>
<h2 id="-速查表30-秒做出正确选择">🎯 速查表:30 秒做出正确选择</h2>
<table>
<thead>
<tr>
<th>场景</th>
<th>特征</th>
<th>推荐技术</th>
<th>避免</th>
<th>性能提升</th>
</tr>
</thead>
<tbody>
<tr>
<td>Web API 调用</td>
<td>等待网络响应</td>
<td><code>async/await</code> + <code>HttpClient</code></td>
<td><code>Task.Run</code></td>
<td>10-100x</td>
</tr>
<tr>
<td>数据库查询</td>
<td>等待数据库响应</td>
<td><code>async/await</code> + EF Core Async</td>
<td><code>.Result</code> / <code>.Wait()</code></td>
<td>10-100x</td>
</tr>
<tr>
<td>文件读写</td>
<td>等待磁盘 I/O</td>
<td><code>async/await</code> + <code>Stream</code></td>
<td>同步 I/O</td>
<td>5-20x</td>
</tr>
<tr>
<td>图像处理</td>
<td>CPU 计算</td>
<td><code>Parallel.ForEach</code></td>
<td><code>async/await</code></td>
<td>2.5-3.5x</td>
</tr>
<tr>
<td>视频编码</td>
<td>CPU 计算</td>
<td><code>Parallel</code> + <code>Task.Run</code></td>
<td>单线程</td>
<td>2.5-3.5x</td>
</tr>
<tr>
<td>数据分析</td>
<td>CPU 计算</td>
<td><code>PLINQ</code></td>
<td>普通 LINQ</td>
<td>2.5-3.5x</td>
</tr>
<tr>
<td>批量下载</td>
<td>I/O 密集</td>
<td><code>Task.WhenAll</code> + <code>async/await</code></td>
<td>串行下载</td>
<td>文件数倍</td>
</tr>
<tr>
<td>混合任务</td>
<td>I/O + CPU</td>
<td>组合使用</td>
<td>全用异步或全用并行</td>
<td>5-20x</td>
</tr>
</tbody>
</table>
<p><strong>记住一个原则</strong>:</p>
<ul>
<li><strong>等 I/O → async/await</strong></li>
<li><strong>等 CPU → Parallel/PLINQ</strong></li>
</ul>
<hr>
<h2 id="-本章小结从困惑到清晰">📌 本章小结:从困惑到清晰</h2>
<h3 id="核心洞察">核心洞察</h3>
<ol>
<li>
<p><strong>并发、并行、异步的本质</strong>:</p>
<ul>
<li>并发 = 组织代码的方式(逻辑层面)</li>
<li>并行 = 利用多核硬件(物理层面)</li>
<li>异步 = 不浪费等待时间(执行模式)</li>
</ul>
</li>
<li>
<p><strong>技术选择的黄金法则</strong>:</p>
<ul>
<li>I/O 密集 → <code>async/await</code>(释放线程)</li>
<li>CPU 密集 → <code>Parallel</code>(利用多核)</li>
<li>混合型 → 组合使用</li>
</ul>
</li>
<li>
<p><strong>常见误区的根源</strong>:</p>
<ul>
<li>Task ≠ 线程</li>
<li>async ≠ 多线程</li>
<li>Task.Run ≠ 性能提升</li>
</ul>
</li>
<li>
<p><strong>性能优化的真相</strong>:</p>
<ul>
<li>不是"让代码跑得快"</li>
<li>而是"不浪费资源"</li>
</ul>
</li>
</ol>
<hr>
<h2 id="-思考题">💭 思考题</h2>
<h3 id="问题-1为什么异步能提升吞吐量但不一定能降低响应时间">问题 1:为什么异步能提升吞吐量,但不一定能降低响应时间?</h3>
<hr>
<blockquote>
<p><strong>💡 答案解析</strong></p>
<p><strong>吞吐量 vs 响应时间</strong>:</p>
<ul>
<li>吞吐量(Throughput)= 单位时间处理的请求数</li>
<li>响应时间(Latency)= 单个请求的完成时间</li>
</ul>
<p>异步的核心是<strong>释放线程</strong>,让一个线程能处理更多请求:</p>
<ul>
<li>同步:100 个线程 → 处理 100 个请求</li>
<li>异步:100 个线程 → 处理 10000 个请求(线程被复用)</li>
</ul>
<p>但单个请求的耗时(如网络延迟 100ms)不会变。</p>
<p><strong>要降低响应时间,需要</strong>:</p>
<ul>
<li>并发执行(Task.WhenAll)</li>
<li>缓存</li>
<li>更快的网络/数据库</li>
</ul>
</blockquote>
<hr>
<h3 id="问题-2下面的代码有什么问题">问题 2:下面的代码有什么问题?</h3>
<pre><code class="language-csharp">public async Task ProcessDataAsync()
{
var data = await DownloadDataAsync();// I/O
var result = await Task.Run(() =>
{
return data.Select(x => x * 2).ToList();// CPU?
});
}
</code></pre>
<hr>
<blockquote>
<p><strong>💡 答案解析</strong></p>
<p><strong>问题</strong>:<code>Select</code> 操作不是 CPU 密集型,不需要 <code>Task.Run</code>。</p>
<p><strong>改进</strong>:</p>
<pre><code class="language-csharp">public async Task ProcessDataAsync()
{
var data = await DownloadDataAsync();// I/O
var result = data.Select(x => x * 2).ToList();// 同步即可
}
</code></pre>
<p><strong>什么时候需要 Task.Run?</strong></p>
<ul>
<li>循环次数 > 10000</li>
<li>单次循环耗时 > 1ms</li>
<li>总耗时 > 100ms</li>
</ul>
<p>简单的 LINQ 操作不需要!</p>
</blockquote>
<hr>
<h3 id="问题-3为什么-aspnet-core-强烈建议全部使用异步">问题 3:为什么 ASP.NET Core 强烈建议全部使用异步?</h3>
<hr>
<blockquote>
<p><strong>💡 答案解析</strong></p>
<p><strong>Web 应用的特点</strong>:</p>
<ul>
<li>99% 的时间在等 I/O(数据库、缓存、外部 API)</li>
<li>请求数量大(可能同时有数千个请求)</li>
</ul>
<p><strong>同步的问题</strong>:</p>
<ul>
<li>100 个线程 → 只能处理 100 个并发请求</li>
<li>第 101 个请求被拒绝(HTTP 503)</li>
</ul>
<p><strong>异步的优势</strong>:</p>
<ul>
<li>100 个线程 → 可以处理 10000+ 个并发请求</li>
<li>吞吐量提升 <strong>100 倍</strong></li>
</ul>
<p><strong>实际数据</strong>(微软官方测试):</p>
<ul>
<li>同步:1000 并发 → CPU 100%,响应时间 5s</li>
<li>异步:1000 并发 → CPU 20%,响应时间 100ms</li>
</ul>
</blockquote>
<hr>
<h2 id="-下一步">🚀 下一步</h2>
<p>在下一章《02. 线程的底层:Thread、ThreadPool 与 Task 的关系》中,我们将:</p>
<ul>
<li>用代码实验<strong>验证 Thread 的真实成本</strong>(内存、上下文切换)</li>
<li>观察 <strong>ThreadPool 的工作窃取算法</strong>(为什么比你手动管理更高效)</li>
<li>理解 <strong>Task 如何在底层调度</strong>(状态机、线程复用)</li>
<li>回答<strong>为什么现代 .NET 推荐 Task 而不是 Thread</strong></li>
</ul>
<p><strong>预告一个震撼的实验</strong>:</p>
<pre><code class="language-csharp">// 创建 10000 个 Thread:内存占用 10GB+,系统崩溃
// 创建 10000 个 Task:内存占用 < 100MB,完美运行
</code></pre>
<hr>
<p><strong>示例代码仓库</strong>:https://github.com/Naughtyhusky/csharp-concurrency-cookbook</p>
<p><strong>有问题?欢迎留言讨论!</strong></p><br><br>
来源:https://www.cnblogs.com/diamondhusky/p/19856073
頁:
[1]