【.NET并发编程 - 04】 async/await 原理与性能优化:深入理解异步编程
<h1 id="04-asyncawait-原理与性能优化深入理解异步编程">04. async/await 原理与性能优化:深入理解异步编程</h1><blockquote>
<p><strong>本章 GitHub 仓库</strong>:csharp-concurrency-cookbook ⭐</p>
<p>欢迎 Star 和 Fork!所有代码示例都可以在仓库中找到并运行。</p>
</blockquote>
<hr>
<h2 id="-本章导读">🎯 本章导读</h2>
<blockquote>
<p>📌 <strong>本文目标</strong>:深入理解 async/await 的编译器魔法,掌握性能优化技巧,写出高效的异步代码。</p>
</blockquote>
<p>在上一章中,我们系统学习了 Task API 的核心方法和属性。本章将揭开 async/await 的神秘面纱,回答以下核心问题:</p>
<ul>
<li><strong>为什么需要 async/await</strong>:.NET Core 为何主推异步编程?async/await 解决了什么问题?</li>
<li><strong>编译器魔法</strong>:async/await 背后发生了什么?状态机是如何工作的?</li>
<li><strong>线程真相</strong>:为什么说 async/await 不等于多线程?</li>
<li><strong>性能优化</strong>:ValueTask 解决了什么核心问题?为什么 .NET Core 源码大量使用它?</li>
<li><strong>常见陷阱</strong>:async void、死锁、async 传染性</li>
</ul>
<blockquote>
<p>⚠️ <strong>重要提示</strong>:本文将深入编译器生成的代码,建议先掌握第 03 章的 Task API 基础。</p>
</blockquote>
<hr>
<h2 id="0️⃣-为什么-net-core-主推异步编程">0️⃣ 为什么 .NET Core 主推异步编程?</h2>
<h3 id="01-异步编程的演进从地狱到天堂">0.1 异步编程的演进:从地狱到天堂</h3>
<p>在 async/await 出现之前,.NET 开发者是如何写异步代码的?让我们看看那段"黑暗"的历史。</p>
<p><strong>场景</strong>:下载一个网页内容,解析 JSON,保存到数据库。</p>
<h4 id="-方式-1thread-手动管理2005-年的噩梦">❌ 方式 1:Thread 手动管理(2005 年的噩梦)</h4>
<pre><code class="language-csharp">public void DownloadAndSaveData(string url)
{
// 创建新线程
Thread thread = new Thread(() =>
{
try
{
// 1. 下载数据
var client = new WebClient();
string json = client.DownloadString(url);
// 2. 解析 JSON
var data = JsonSerializer.Deserialize<MyData>(json);
// 3. 保存到数据库(又要新建一个线程?)
Thread dbThread = new Thread(() =>
{
try
{
// 数据库操作...
SaveToDatabase(data);
// 4. 更新 UI(必须回到 UI 线程!)
this.Invoke(new Action(() =>
{
MessageBox.Show("保存成功");
}));
}
catch (Exception ex)
{
// 错误处理...
this.Invoke(new Action(() =>
{
MessageBox.Show($"数据库错误: {ex.Message}");
}));
}
});
dbThread.Start();
}
catch (Exception ex)
{
// 错误处理...
this.Invoke(new Action(() =>
{
MessageBox.Show($"下载错误: {ex.Message}");
}));
}
});
thread.Start();
}
</code></pre>
<p><strong>问题一大堆</strong>:</p>
<ul>
<li>😱 <strong>回调地狱</strong>:嵌套 3-4 层,根本看不懂逻辑</li>
<li>😱 <strong>线程切换混乱</strong>:需要手动 <code>Invoke</code> 回到 UI 线程</li>
<li>😱 <strong>异常处理困难</strong>:每一层都要 try-catch</li>
<li>😱 <strong>资源泄漏风险</strong>:忘记 <code>Join</code> 或 <code>Dispose</code></li>
<li>😱 <strong>无法取消</strong>:如何取消一个正在执行的线程?</li>
<li>😱 <strong>性能差</strong>:每个操作都创建新线程(Thread 的栈内存是 1MB!)</li>
</ul>
<h4 id="-方式-2apmbeginend-模式2010-年的改进">❌ 方式 2:APM(Begin/End 模式,2010 年的改进)</h4>
<pre><code class="language-csharp">public void DownloadAndSaveData(string url)
{
var request = WebRequest.Create(url);
// 开始异步请求
request.BeginGetResponse(ar1 =>
{
try
{
var response = request.EndGetResponse(ar1);
var stream = response.GetResponseStream();
// 读取流
var buffer = new byte;
stream.BeginRead(buffer, 0, buffer.Length, ar2 =>
{
try
{
int bytesRead = stream.EndRead(ar2);
string json = Encoding.UTF8.GetString(buffer, 0, bytesRead);
// 解析 JSON
var data = JsonSerializer.Deserialize<MyData>(json);
// 保存到数据库...
// 又是一堆 Begin/End...
}
catch (Exception ex)
{
// 错误处理...
}
}, null);
}
catch (Exception ex)
{
// 错误处理...
}
}, null);
}
</code></pre>
<p><strong>仍然很痛苦</strong>:</p>
<ul>
<li>😱 <strong>回调地狱</strong>:Begin/End 套娃</li>
<li>😱 <strong>状态传递困难</strong>:需要通过 <code>AsyncState</code> 传递上下文</li>
<li>😱 <strong>代码割裂</strong>:逻辑被分成多个回调函数</li>
<li>😱 <strong>难以理解</strong>:新手根本看不懂</li>
</ul>
<h4 id="-方式-3asyncawait2012-年的革命">✅ 方式 3:async/await(2012 年的革命)</h4>
<pre><code class="language-csharp">public async Task DownloadAndSaveDataAsync(string url)
{
// 1. 下载数据
using var client = new HttpClient();
string json = await client.GetStringAsync(url);
// 2. 解析 JSON
var data = JsonSerializer.Deserialize<MyData>(json);
// 3. 保存到数据库
await SaveToDatabaseAsync(data);
// 4. 更新 UI(自动回到 UI 线程!)
MessageBox.Show("保存成功");
}
</code></pre>
<p><strong>优雅得令人感动</strong>:</p>
<ul>
<li>✅ <strong>同步写法,异步执行</strong>:代码看起来像同步,但不阻塞线程</li>
<li>✅ <strong>自动上下文切换</strong>:<code>await</code> 后自动回到 UI 线程</li>
<li>✅ <strong>异常处理简单</strong>:用普通的 try-catch 就行</li>
<li>✅ <strong>可取消</strong>:支持 <code>CancellationToken</code>(后面章节会讲)</li>
<li>✅ <strong>性能好</strong>:I/O 操作不占用线程</li>
</ul>
<hr>
<h3 id="02-asyncawait-解决了什么核心问题">0.2 async/await 解决了什么核心问题?</h3>
<p>通过上面的对比,我们可以看到 async/await 解决了三个核心问题:</p>
<h4 id="1-消灭回调地狱callback-hell">1. <strong>消灭回调地狱(Callback Hell)</strong></h4>
<p><strong>问题</strong>:传统异步模型(APM、EAP)使用回调函数,导致代码嵌套深、难以理解。</p>
<p><strong>解决</strong>:async/await 让你用<strong>同步的写法</strong>写异步代码,编译器帮你生成状态机。</p>
<pre><code class="language-csharp">// 回调地狱
BeginOp1(() => {
BeginOp2(() => {
BeginOp3(() => {
// 😱 嵌套 N 层
});
});
});
// async/await
await Op1Async();
await Op2Async();
await Op3Async();
// ✅ 线性代码,清晰易懂
</code></pre>
<h4 id="2-自动上下文管理">2. <strong>自动上下文管理</strong></h4>
<p><strong>问题</strong>:手动管理线程切换(UI 线程、同步上下文)非常容易出错。</p>
<p><strong>解决</strong>:<code>await</code> 会自动捕获当前的 <code>SynchronizationContext</code>,并在任务完成后恢复到原来的上下文。</p>
<blockquote>
<p>📝 <strong>注意</strong>:关于 <code>SynchronizationContext</code> 和 <code>ConfigureAwait</code>,我们会在后面的章节(第 05 章)详细讲解。这里只需要知道:async/await 帮你自动处理了线程切换的复杂性。</p>
</blockquote>
<h4 id="3-高效的-io-操作">3. <strong>高效的 I/O 操作</strong></h4>
<p><strong>问题</strong>:传统的同步 I/O 会阻塞线程,浪费资源;手动创建线程又开销太大。</p>
<p><strong>解决</strong>:async/await 配合异步 I/O API(如 <code>HttpClient.GetStringAsync</code>),可以在 I/O 等待期间<strong>释放线程</strong>,不阻塞、不浪费。</p>
<p><strong>这就是为什么 .NET Core 主推异步编程</strong>:</p>
<ul>
<li>ASP.NET Core:单台服务器可以处理更多并发请求(从 1000+ 到 10000+)</li>
<li>Blazor:UI 不会卡顿</li>
<li>后台服务:更高的吞吐量</li>
</ul>
<hr>
<h3 id="03-asyncawait-的本质">0.3 async/await 的本质</h3>
<p>在深入原理之前,先记住这个核心概念:</p>
<blockquote>
<p><strong>async/await 只是语法糖,编译器会把你的异步方法转换成一个状态机。</strong></p>
</blockquote>
<p>这个状态机:</p>
<ul>
<li>记住了"当前执行到哪一步"</li>
<li>在 <code>await</code> 时"暂停"执行(但不阻塞线程)</li>
<li>在任务完成后"恢复"执行</li>
</ul>
<p>接下来,我们就来揭开这个"编译器魔法"的神秘面纱。</p>
<hr>
<h2 id="1️⃣-asyncawait-基础回顾">1️⃣ async/await 基础回顾</h2>
<h3 id="11-最简单的异步方法">1.1 最简单的异步方法</h3>
<p>现在你已经理解了 async/await 的价值,让我们从最简单的例子开始:<br>
现在,让我们看看 async/await 是如何优雅地解决这个问题的:</p>
<pre><code class="language-csharp">public async Task<string> DownloadContentAsync(string url)
{
using HttpClient client = new HttpClient();
string content = await client.GetStringAsync(url);
return content;
}
</code></pre>
<p>这段代码做了什么呢?它从指定的 URL 下载内容,但<strong>不会阻塞线程</strong>。让我们拆解一下:</p>
<ol>
<li><strong><code>async</code> 关键字</strong>:告诉编译器"这是一个异步方法,请帮我生成状态机"</li>
<li><strong><code>await</code> 关键字</strong>:在这里等待下载完成,但<strong>不阻塞线程</strong>(线程会被释放去做其他事情)</li>
<li><strong><code>Task<string></code> 返回类型</strong>:表示这个方法会异步返回一个字符串</li>
<li><strong><code>Async</code> 后缀</strong>:这是约定俗成的命名规范,一眼就能看出这是异步方法</li>
</ol>
<p><strong>与第 03 章的对比</strong>:</p>
<pre><code class="language-csharp">// 第 03 章的方式:阻塞线程
public string DownloadContent(string url)
{
using HttpClient client = new HttpClient();
Task<string> task = client.GetStringAsync(url);
return task.Result; // ⚠️ 阻塞当前线程,浪费资源
}
// 现在的方式:不阻塞线程
public async Task<string> DownloadContentAsync(string url)
{
using HttpClient client = new HttpClient();
string content = await client.GetStringAsync(url); // ✅ 释放线程
return content;
}
</code></pre>
<p><strong>关键要素总结</strong>:</p>
<ul>
<li>✅ <code>async</code> 关键字:标记方法为异步方法</li>
<li>✅ <code>await</code> 关键字:异步等待一个 Task</li>
<li>✅ 返回类型:<code>Task<T></code> 或 <code>Task</code> 或 <code>ValueTask<T></code></li>
<li>✅ 方法命名:通常以 <code>Async</code> 结尾</li>
</ul>
<hr>
<h3 id="12-asyncawait-的三种返回类型">1.2 async/await 的三种返回类型</h3>
<p>async 方法可以有三种返回类型(其实是四种,但有一种很危险):</p>
<table>
<thead>
<tr>
<th>返回类型</th>
<th>使用场景</th>
<th>示例</th>
<th>说明</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>Task</code></td>
<td>无返回值的异步方法</td>
<td><code>async Task SaveDataAsync()</code></td>
<td>类似于 <code>void</code>,但可以 await</td>
</tr>
<tr>
<td><code>Task<T></code></td>
<td>有返回值的异步方法</td>
<td><code>async Task<int> GetCountAsync()</code></td>
<td>返回一个结果</td>
</tr>
<tr>
<td><code>ValueTask<T></code></td>
<td>高性能场景(.NET Core 2.1+)</td>
<td><code>async ValueTask<int> GetCachedAsync()</code></td>
<td>本章后面会详细讲</td>
</tr>
<tr>
<td><code>void</code></td>
<td><strong>⚠️ 仅用于事件处理器</strong></td>
<td><code>async void Button_Click()</code></td>
<td><strong>危险!异常无法捕获</strong></td>
</tr>
</tbody>
</table>
<blockquote>
<p>⚠️ <strong>警告</strong>:<code>async void</code> 无法捕获异常,一旦出错就会导致应用崩溃!除了事件处理器(如按钮点击),<strong>永远不要使用</strong> <code>async void</code>!</p>
</blockquote>
<hr>
<h3 id="13-asyncawait-的语义规则">1.3 async/await 的语义规则</h3>
<p>让我们通过几个例子来理解 async/await 的行为:</p>
<pre><code class="language-csharp">// ❌ 错误示例:没有 await 的 async 方法
public async Task Method1()
{
Console.WriteLine("没有 await");
// ⚠️ 编译器警告:CS1998: 此异步方法缺少 'await' 运算符
// 这个 async 是没有意义的,应该去掉
}
// ✅ 正确示例:多个 await
public async Task Method2()
{
await Task.Delay(1000);
Console.WriteLine("第一个 await 完成");
await Task.Delay(1000);
Console.WriteLine("第二个 await 完成");
}
</code></pre>
<p><strong>核心行为</strong>:<code>await</code> 会暂停当前方法的执行,但不会阻塞线程。</p>
<p>想象一下,你在餐厅点了一杯咖啡:</p>
<ul>
<li><strong>同步阻塞</strong>(<code>task.Result</code>):你站在柜台前死死盯着咖啡师,什么都不做,直到咖啡做好 ⏳</li>
<li><strong>async/await</strong>:你点完咖啡后去找个座位坐下,咖啡师做好后会叫你。这期间你可以玩手机、看书 ✅</li>
</ul>
<pre><code class="language-csharp">// await 可以在任何位置
public async Task<int> Method3()
{
// 1. 启动任务(不等待)
var task = Task.Run(() => 42);
Console.WriteLine("任务已启动,我可以先做点别的事");
// 2. 现在等待任务完成
int result = await task;
Console.WriteLine($"任务完成,结果是: {result}");
return result;
}
</code></pre>
<p><strong>核心规则总结</strong>:</p>
<ul>
<li>✅ <code>async</code> 方法必须包含至少一个 <code>await</code>(否则编译器会警告)</li>
<li>✅ <code>await</code> 会暂停当前方法的执行(但不阻塞线程)</li>
<li>✅ <code>await</code> 之后的代码会在 awaited 任务完成后继续执行</li>
<li>✅ <code>await</code> 不会阻塞线程(对于 I/O 操作)——这是最重要的!</li>
</ul>
<hr>
<h2 id="2️⃣-编译器魔法状态机揭秘">2️⃣ 编译器魔法:状态机揭秘</h2>
<p>现在是本章最精彩的部分!你知道吗?<strong>async/await 只是语法糖</strong>,编译器会把你的异步方法转换成一个"状态机"。</p>
<p>听起来很复杂?别担心,让我们一步步揭开这个魔法的面纱。</p>
<hr>
<h3 id="21-一个简单的-async-方法">2.1 一个简单的 async 方法</h3>
<p>先看一个最简单的例子:</p>
<pre><code class="language-csharp">public async Task<string> GetDataAsync()
{
Console.WriteLine("开始");
await Task.Delay(1000);
Console.WriteLine("完成");
return "Data";
}
</code></pre>
<p>这个方法做了什么?</p>
<ol>
<li>打印"开始"</li>
<li>等待 1 秒(<code>await Task.Delay(1000)</code>)</li>
<li>打印"完成"</li>
<li>返回字符串 "Data"</li>
</ol>
<p>看起来很简单,对吧?但编译器在背后做了大量的工作!</p>
<hr>
<h3 id="22-编译器生成的状态机简化版">2.2 编译器生成的状态机(简化版)</h3>
<p>当你编译上面的代码时,C# 编译器会把它转换成一个"状态机"。什么是状态机?就是一个用 <code>switch-case</code> 实现的、能够记住当前执行到哪一步的对象。</p>
<p>让我们看看编译器生成的代码(简化版,方便理解):</p>
<pre><code class="language-csharp">// 编译器生成的状态机(简化版)
struct GetDataAsyncStateMachine : IAsyncStateMachine
{
public int State; // 当前状态(0 或 1)
public AsyncTaskMethodBuilder<string> Builder;// 用于创建和完成 Task
private TaskAwaiter Awaiter; // 保存 awaiter
public void MoveNext()
{
try
{
switch (State)
{
case 0: // 初始状态
Console.WriteLine("开始");
// 创建 awaiter
Awaiter = Task.Delay(1000).GetAwaiter();
if (Awaiter.IsCompleted)
{
// 同步完成(快速路径)
goto case 1;
}
else
{
// 异步完成(慢速路径)
State = 1; // 记住下次从状态 1 开始
Awaiter.OnCompleted(MoveNext); // 注册回调
return; // ⭐ 暂停执行,释放线程
}
case 1: // await 完成后的状态
Awaiter.GetResult(); // 获取结果或抛出异常
Console.WriteLine("完成");
// 设置结果
Builder.SetResult("Data");
return;
}
}
catch (Exception ex)
{
Builder.SetException(ex);
}
}
}
</code></pre>
<p><strong>看懂了吗?让我们拆解一下:</strong></p>
<ol>
<li><strong>State 字段</strong>:记录当前执行到哪个 <code>await</code>(0 表示第一个 await 之前,1 表示第一个 await 之后)</li>
<li><strong>MoveNext 方法</strong>:状态机的核心,用 <code>switch-case</code> 实现状态转换</li>
<li><strong>Awaiter 字段</strong>:保存每个 <code>await</code> 的 awaiter(用于恢复执行)</li>
<li><strong>AsyncTaskMethodBuilder</strong>:负责创建和完成 Task</li>
</ol>
<p><strong>关键点</strong>:当 <code>await</code> 的任务还没完成时,状态机会:</p>
<ol>
<li>记录当前状态(<code>State = 1</code>)</li>
<li>注册一个回调(<code>Awaiter.OnCompleted(MoveNext)</code>)</li>
<li><strong>立即返回</strong>(释放线程)⭐</li>
</ol>
<p>当任务完成后,回调会被调用,状态机会从上次的状态继续执行。</p>
<hr>
<h3 id="23-状态机的执行流程可视化">2.3 状态机的执行流程(可视化)</h3>
<p>让我们用一张流程图来理解状态机的执行过程:</p>
<div class="mermaid">flowchart TD
Start([调用 GetDataAsync]) --> CreateSM[创建状态机<br/>State = 0]
CreateSM --> CallMoveNext[调用 MoveNext]
CallMoveNext --> Case0{State = 0}
Case0 -->|初始状态| Print1
Print1 --> CreateAwaiter[创建 Task.Delay1000.GetAwaiter]
CreateAwaiter --> CheckComplete{Awaiter.IsCompleted?}
CheckComplete -->|是 同步完成| SyncPath[同步路径<br/>直接跳到 case 1]
SyncPath --> Print2
Print2 --> SetResult
SetResult --> ReturnTask[返回已完成的 Task]
CheckComplete -->|否 异步完成| AsyncPath[异步路径]
AsyncPath --> SetState1
SetState1 --> RegisterCallback
RegisterCallback --> ReturnIncomplete[返回未完成的 Task<br/>释放线程]
ReturnIncomplete -.等待.-> DelayComplete
DelayComplete --> CallMoveNext2[调用 MoveNext<br/>可能在不同线程]
CallMoveNext2 --> Case1{State = 1}
Case1 --> GetResult
GetResult --> Print2
style Start fill:#bbdefb,color:#1565c0,stroke:#1976d2,stroke-width:2px
style ReturnTask fill:#c8e6c9,color:#2e7d32,stroke:#388e3c,stroke-width:2px
style SyncPath fill:#fff9c4,color:#f57f17,stroke:#f9a825,stroke-width:2px
style AsyncPath fill:#ffccbc,color:#d84315,stroke:#e64a19,stroke-width:2px
style ReturnIncomplete fill:#ffccbc,color:#d84315,stroke:#e64a19,stroke-width:2px
style DelayComplete fill:#e1bee7,color:#6a1b9a,stroke:#7b1fa2,stroke-width:2px
</div><p><strong>流程说明(重点理解)</strong>:</p>
<p>状态机有两条路径:</p>
<p><strong>1. 同步路径(快速路径)⚡</strong></p>
<p>当 <code>Awaiter.IsCompleted == true</code> 时(任务已经完成了):</p>
<ul>
<li>直接跳到下一个状态,继续执行</li>
<li><strong>不会释放线程</strong>,不会有线程切换</li>
<li>非常快!无需等待</li>
</ul>
<p><strong>什么时候会走同步路径?</strong></p>
<ul>
<li>从缓存中获取数据(立即返回)</li>
<li>读取已经在内存中的数据</li>
<li>任务在 await 之前就已经完成了</li>
</ul>
<pre><code class="language-csharp">// 同步路径示例
public async Task<int> GetCachedValueAsync(int key)
{
if (_cache.TryGetValue(key, out int value))
{
return value; // ⚡ 同步返回,无状态切换
}
// 缓存未命中,才走异步路径
return await _database.GetAsync(key);
}
</code></pre>
<p><strong>2. 异步路径(慢速路径)🐌</strong></p>
<p>当 <code>Awaiter.IsCompleted == false</code> 时(任务还没完成):</p>
<ol>
<li>记录当前状态(<code>State = 1</code>)</li>
<li>注册回调(<code>Awaiter.OnCompleted(MoveNext)</code>)</li>
<li><strong>立即返回,释放线程</strong> ⭐</li>
<li>等待任务完成...</li>
<li>任务完成后,回调 <code>MoveNext</code>(可能在不同的线程上)</li>
<li>从上次的状态继续执行</li>
</ol>
<p><strong>这就是 async/await 的魔法所在</strong>:在等待期间,线程被释放了,可以去处理其他请求!</p>
<hr>
<h3 id="24-亲自查看编译器生成的代码">2.4 亲自查看编译器生成的代码</h3>
<p>想看看编译器实际生成的代码吗?用 <strong>SharpLab</strong> 这个神器!</p>
<p>🔗 https://sharplab.io/</p>
<p><strong>步骤</strong>:</p>
<ol>
<li>打开 SharpLab</li>
<li>输入你的 async 方法:</li>
</ol>
<pre><code class="language-csharp">using System;
using System.Threading.Tasks;
public class C {
public async Task<string> M() {
await Task.Delay(1000);
return "Hello";
}
}
</code></pre>
<ol start="3">
<li>选择 "C# -> C#"(查看反编译后的代码)</li>
<li>你会看到编译器生成的完整状态机代码!</li>
</ol>
<p><strong>生成的代码特点</strong>:</p>
<ul>
<li>✅ 状态机结构体(值类型,避免堆分配)</li>
<li>✅ <code>AsyncTaskMethodBuilder</code>(管理 Task 的生命周期)</li>
<li>✅ <code>MoveNext</code> 方法(switch-case 状态转换)</li>
<li>✅ <code>SetStateMachine</code> 方法(用于装箱场景)</li>
</ul>
<p><strong>小提示</strong>:实际生成的代码比我们简化版复杂得多,但核心思想是一样的——用状态机实现"暂停"和"恢复"。</p>
<p>---<strong>生成的代码特点</strong>:</p>
<ul>
<li>✅ 状态机结构体(值类型,避免堆分配)</li>
<li>✅ AsyncTaskMethodBuilder(管理 Task 的生命周期)</li>
<li>✅ MoveNext 方法(switch-case 状态转换)</li>
<li>✅ SetStateMachine 方法(用于装箱场景)</li>
</ul>
<hr>
<h2 id="3️⃣-为什么-asyncawait-不等于多线程">3️⃣ 为什么 async/await 不等于多线程?</h2>
<p>这是一个<strong>超级重要</strong>的概念!很多开发者(包括我刚开始)都以为:</p>
<blockquote>
<p>"加了 <code>async</code>,就会创建新线程,所以就能提升性能"</p>
</blockquote>
<p><strong>大错特错!</strong></p>
<p>让我们彻底搞清楚这个问题。</p>
<hr>
<h3 id="31-常见误解90-的人都犯过">3.1 常见误解(90% 的人都犯过)</h3>
<p>❌ <strong>错误认知</strong>:</p>
<ul>
<li>"加了 <code>async</code> 就会创建新线程"</li>
<li>"<code>await</code> 会在新线程上执行"</li>
<li>"async 方法总是并发执行的"</li>
<li>"async 就是用来提升性能的"</li>
</ul>
<p>✅ <strong>真相(记住这些!)</strong>:</p>
<ul>
<li><strong>async/await 只是语法糖</strong>,生成状态机(前面刚讲过)</li>
<li><strong>I/O 异步操作不占用线程</strong>(使用操作系统的 I/O 完成端口,后面会讲)</li>
<li><strong>CPU 密集型操作仍然需要 <code>Task.Run</code> 创建线程</strong></li>
<li><strong>async/await 的目的是提高吞吐量,而不是降低延迟</strong></li>
</ul>
<blockquote>
<p>📝 <strong>回顾</strong>:在第 02 章《Thread、ThreadPool 与 Task》中,我们讲过 Task 的本质——它是一个异步操作的抽象,不等于线程。现在我们更进一步,理解 async/await 的本质。</p>
</blockquote>
<hr>
<h3 id="32-io-异步不占用线程重点">3.2 I/O 异步:不占用线程(重点)</h3>
<p>让我们做个实验,观察 <code>await</code> 前后的线程 ID:</p>
<pre><code class="language-csharp">public async Task<string> DownloadAsync(string url)
{
Console.WriteLine($" 线程 ID: {Thread.CurrentThread.ManagedThreadId}");
using HttpClient client = new HttpClient();
string content = await client.GetStringAsync(url); // I/O 操作
Console.WriteLine($" 线程 ID: {Thread.CurrentThread.ManagedThreadId}");
return content;
}
</code></pre>
<p><strong>运行结果</strong>:</p>
<pre><code> 线程 ID: 1
线程 ID: 4<-- 可能是不同的线程!
</code></pre>
<p><strong>这说明了什么?</strong></p>
<ol>
<li><strong><code>await</code> 之前和之后的线程 ID 可能不同</strong>(也可能相同,取决于线程池的调度)</li>
<li><strong>在 <code>await</code> 期间,原来的线程被释放了</strong>(回到线程池,去处理其他请求)</li>
<li><strong>网络请求由操作系统的 I/O 完成端口(IOCP)处理</strong>,不需要线程傻等</li>
<li><strong>任务完成后,从线程池取一个线程继续执行</strong></li>
</ol>
<p><strong>关键问题</strong>:那在 <code>await</code> 期间,谁在等待网络响应呢?</p>
<p><strong>答案</strong>:操作系统的 I/O 完成端口(IOCP)!这是操作系统级别的机制,<strong>不需要线程</strong>。</p>
<p>想象一下,你在餐厅点了外卖:</p>
<ul>
<li><strong>同步阻塞方式</strong>:你站在门口死死盯着外卖员,直到他到了为止 ⏳(浪费时间)</li>
<li><strong>async/await 方式</strong>:你点完外卖后继续做其他事(工作、看书),外卖到了会收到通知 ✅(高效)</li>
</ul>
<hr>
<h3 id="33-线程使用对比可视化">3.3 线程使用对比(可视化)</h3>
<p>让我们用一张时序图来对比同步阻塞和 async/await 的线程使用:</p>
<div class="mermaid">sequenceDiagram
participant App as 应用代码
participant TP as ThreadPool
participant T1 as 线程 #1
participant IOCP as I/O 完成端口
participant T2 as 线程 #2
participant Network as 网络
Note over App,Network: 场景 1: 同步阻塞 .Result
App->>TP: 请求线程
TP->>T1: 分配线程 #1
activate T1
T1->>Network: 发送 HTTP 请求
Note right of T1: ⚠️ 线程 #1 被阻塞<br/>什么都不做<br/>浪费资源
Network-->>T1: 返回响应
T1->>App: 返回结果
deactivate T1
Note over App,Network: 场景 2: async/await
App->>TP: 请求线程
TP->>T1: 分配线程 #1
activate T1
T1->>IOCP: 注册异步 I/O<br/>提交请求
T1->>TP: 归还线程 #1<br/>⭐ 线程被释放
deactivate T1
IOCP->>Network: 发送 HTTP 请求
Note right of IOCP: 无线程等待<br/>节省资源
Network-->>IOCP: 返回响应
IOCP->>TP: I/O 完成通知
TP->>T2: 分配线程 #2<br/>可能是不同的线程
activate T2
T2->>App: 继续执行 await 后的代码
deactivate T2
</div><p><strong>对比总结(一目了然)</strong>:</p>
<table>
<thead>
<tr>
<th>特性</th>
<th>同步阻塞(.Result)</th>
<th>async/await</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>线程使用</strong></td>
<td>1 个线程全程阻塞 ❌</td>
<td>0 个线程等待(I/O 期间)✅</td>
</tr>
<tr>
<td><strong>资源消耗</strong></td>
<td>高(线程 + 1MB 栈内存)❌</td>
<td>低(无线程等待)✅</td>
</tr>
<tr>
<td><strong>吞吐量</strong></td>
<td>低(线程池耗尽)❌</td>
<td>高(线程可处理其他请求)✅</td>
</tr>
<tr>
<td><strong>适用场景</strong></td>
<td>控制台应用、脚本</td>
<td>Web API、UI 应用 ⭐</td>
</tr>
</tbody>
</table>
<p><strong>举个实际例子</strong>:</p>
<p>假设你的 Web API 有 100 个线程,同时收到 200 个请求:</p>
<ul>
<li><strong>同步阻塞方式</strong>:前 100 个请求占满所有线程,后 100 个请求排队等待(用户体验差)</li>
<li><strong>async/await 方式</strong>:100 个线程可以处理 200 个请求(在等待数据库、网络响应时释放线程)</li>
</ul>
<p>这就是为什么 ASP.NET Core 建议所有 I/O 操作都用 async/await!</p>
<hr>
<h3 id="34-cpu-密集型需要-taskrun">3.4 CPU 密集型:需要 Task.Run</h3>
<p>前面说了,I/O 操作直接 <code>await</code> 就好。但如果是 CPU 密集型操作呢?</p>
<p><strong>CPU 密集型操作</strong>:大量计算、图像处理、数据分析等(在第 01 章《并发编程全景图》中我们讲过)。</p>
<pre><code class="language-csharp">// ❌ 错误:async 不会自动创建线程
public async Task<int> CalculatePrimesAsync(int max)
{
// ⚠️ 这段代码仍然在当前线程上执行!
int count = 0;
for (int i = 2; i <= max; i++)
{
if (IsPrime(i)) count++;
}
return count; // 编译器警告:CS1998(没有 await)
}
</code></pre>
<p><strong>为什么是错误的?</strong></p>
<ul>
<li>虽然方法名叫 <code>xxxAsync</code>,但实际上是<strong>同步执行</strong>的</li>
<li>会阻塞当前线程(可能是 UI 线程或 Web API 的请求线程)</li>
<li>没有任何异步的好处</li>
</ul>
<p><strong>正确做法</strong>:</p>
<pre><code class="language-csharp">// ✅ 正确:使用 Task.Run 创建线程
public async Task<int> CalculatePrimesAsync(int max)
{
return await Task.Run(() =>
{
int count = 0;
for (int i = 2; i <= max; i++)
{
if (IsPrime(i)) count++;
}
return count;
});
}
</code></pre>
<p><strong>为什么是正确的?</strong></p>
<ul>
<li><code>Task.Run</code> 会把任务放到线程池执行(在第 03 章《Task API 完全指南》中讲过)</li>
<li>不会阻塞当前线程</li>
<li>真正的异步执行</li>
</ul>
<p><strong>关键规则总结</strong>:</p>
<ul>
<li>✅ <strong>I/O 操作</strong>:直接 <code>await</code>(如网络、文件、数据库)</li>
<li>✅ <strong>CPU 密集型</strong>:使用 <code>Task.Run</code> 放到线程池执行</li>
<li>❌ <strong>避免</strong>:<code>await Task.Run(async () => await ...)</code>(双重异步,没必要)</li>
</ul>
<pre><code class="language-csharp">// ❌ 错误:I/O 操作不需要 Task.Run
public async Task<string> GetDataAsync()
{
return await Task.Run(async () =>
{
using HttpClient client = new HttpClient();
return await client.GetStringAsync("https://api.example.com");
}); // 多此一举!浪费了一个线程
}
// ✅ 正确:I/O 操作直接 await
public async Task<string> GetDataAsync()
{
using HttpClient client = new HttpClient();
return await client.GetStringAsync("https://api.example.com");
}
</code></pre>
<hr>
<h2 id="4️⃣-性能优化valuetask-的核心价值">4️⃣ 性能优化:ValueTask 的核心价值</h2>
<p>现在进入性能优化的核心部分。你可能会问:<strong>既然有了 Task,为什么微软还要创造一个 ValueTask?它解决了 Task 解决不了的什么问题?</strong></p>
<hr>
<h3 id="41-task-的性能瓶颈">4.1 Task<t> 的性能瓶颈</t></h3>
<p><strong>场景</strong>:高频调用的异步方法(如缓存访问、数据库查询)。</p>
<p>让我们看一个典型案例:</p>
<pre><code class="language-csharp">// 使用 Task<T>
public async Task<User?> GetUserAsync(int userId)
{
// 1. 先查内存缓存
if (_memoryCache.TryGetValue(userId, out User? cachedUser))
{
return cachedUser; // ⚠️ 同步返回,但会创建 Task<User> 对象
}
// 2. 缓存未命中,查数据库
return await _database.GetUserAsync(userId);
}
</code></pre>
<p><strong>问题分析</strong>:</p>
<p>即使缓存命中了(90% 的情况),编译器也会创建一个 <code>Task<User></code> 对象:</p>
<pre><code class="language-csharp">// 编译器生成的代码(简化)
if (_memoryCache.TryGetValue(userId, out User? cachedUser))
{
return Task.FromResult(cachedUser); // 💥 堆分配!
}
</code></pre>
<p><strong>为什么是问题?</strong></p>
<ol>
<li><strong>Task<t> 是引用类型(class)</t></strong>:每次创建都在堆上分配内存</li>
<li><strong>高频调用场景</strong>:假设每秒 10,000 次请求,90% 缓存命中率
<ul>
<li>每秒分配 9,000 个 Task<user> 对象</user></li>
<li>假设每个 Task 对象 48 字节:9,000 × 48 = 432 KB/秒</li>
<li><strong>每秒约 432 KB 的垃圾</strong>!</li>
</ul>
</li>
<li><strong>GC 压力</strong>:频繁的分配导致 Gen0 GC 频繁触发</li>
</ol>
<p><strong>对于高性能场景(Web API、游戏服务器、金融系统),这是不可接受的。</strong></p>
<hr>
<h3 id="42-valuetask-的核心创新">4.2 ValueTask<t> 的核心创新</t></h3>
<p><strong>核心思想</strong>:既然大部分情况下是同步完成的,为什么不用<strong>值类型(struct)</strong>来避免堆分配呢?</p>
<pre><code class="language-csharp">// 使用 ValueTask<T>
public async ValueTask<User?> GetUserAsync(int userId)
{
// 1. 先查内存缓存
if (_memoryCache.TryGetValue(userId, out User? cachedUser))
{
return cachedUser; // ✅ 无堆分配!值直接包装在 struct 中
}
// 2. 缓存未命中,查数据库
return await _database.GetUserAsync(userId);
}
</code></pre>
<p><strong>ValueTask<t> 的魔法</t></strong>:</p>
<pre><code class="language-csharp">// ValueTask<T> 的简化实现
public readonly struct ValueTask<T>
{
private readonly T _result; // 同步完成时的结果
private readonly Task<T>? _task; // 异步完成时的 Task
// 同步完成:直接包装结果
public ValueTask(T result)
{
_result = result;
_task = null; // 无 Task 分配
}
// 异步完成:包装 Task
public ValueTask(Task<T> task)
{
_result = default!;
_task = task;
}
}
</code></pre>
<p><strong>工作原理</strong>:</p>
<ul>
<li><strong>缓存命中(同步)</strong>:值直接存储在 <code>ValueTask<T></code> 的 <code>_result</code> 字段中,<strong>无堆分配</strong> ⚡</li>
<li><strong>缓存未命中(异步)</strong>:内部包装一个 <code>Task<T></code>,行为和 Task 一样</li>
</ul>
<hr>
<h3 id="43-net-core-源码中的-valuetask-实战">4.3 .NET Core 源码中的 ValueTask 实战</h3>
<p>微软在 .NET Core 的核心库中大量使用 ValueTask。让我们看几个真实案例:</p>
<h4 id="案例-1streamreadasync最典型的例子">案例 1:Stream.ReadAsync(最典型的例子)</h4>
<p><strong>问题</strong>:在 .NET Framework 时代,<code>Stream.ReadAsync</code> 返回 <code>Task<int></code>。</p>
<pre><code class="language-csharp">// .NET Framework (旧版本)
public virtual Task<int> ReadAsync(byte[] buffer, int offset, int count)
{
// 即使缓冲区有数据(同步完成),也要创建 Task
return Task.FromResult(bytesRead); // 💥 堆分配
}
</code></pre>
<p><strong>优化</strong>:.NET Core 改用 <code>ValueTask<int></code>。</p>
<p><strong>源码位置</strong>:<code>System.IO.Stream</code>(.NET Core 3.0+)</p>
<pre><code class="language-csharp">// .NET Core 3.0+ (简化)
public virtual ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken = default)
{
// 1. 如果缓冲区有数据,同步返回
if (_bufferCount > 0)
{
int bytesRead = Math.Min(buffer.Length, _bufferCount);
_buffer.AsSpan(0, bytesRead).CopyTo(buffer.Span);
_bufferCount -= bytesRead;
return new ValueTask<int>(bytesRead); // ✅ 无分配
}
// 2. 缓冲区为空,异步从底层流读取
return new ValueTask<int>(ReadFromStreamAsync(buffer, cancellationToken));
}
</code></pre>
<p><strong>性能提升</strong>:</p>
<ul>
<li><strong>同步路径</strong>(缓冲区有数据):0 字节分配 ⚡</li>
<li><strong>异步路径</strong>(需要 I/O):仍然分配 Task,但这种情况相对少</li>
</ul>
<p><strong>实际数据</strong>(微软的 Benchmark):</p>
<ul>
<li>使用 <code>Task<int></code>:平均 45 ns,48 字节分配</li>
<li>使用 <code>ValueTask<int></code>:平均 12 ns,<strong>0 字节分配</strong> ⭐</li>
<li><strong>性能提升 3.7x</strong></li>
</ul>
<hr>
<h4 id="案例-2aspnet-core-管道">案例 2:ASP.NET Core 管道</h4>
<p><strong>源码位置</strong>:<code>Microsoft.AspNetCore.Http.HttpContext</code></p>
<pre><code class="language-csharp">// ASP.NET Core 管道中的 ValueTask 使用(简化)
public abstract class HttpContext
{
// Response.WriteAsync 返回 ValueTask
public abstract ValueTask WriteAsync(ReadOnlyMemory<byte> data, CancellationToken cancellationToken = default);
}
// 实现(Kestrel 服务器)
internal sealed class DefaultHttpResponse : HttpResponse
{
public override ValueTask WriteAsync(ReadOnlyMemory<byte> data, CancellationToken cancellationToken)
{
// 1. 如果输出缓冲区未满,同步写入
if (_outputBuffer.TryWrite(data))
{
return default; // ✅ ValueTask.CompletedTask,无分配
}
// 2. 缓冲区满,异步刷新并写入
return FlushAndWriteAsync(data, cancellationToken);
}
}
</code></pre>
<p><strong>为什么重要?</strong></p>
<p>ASP.NET Core 每个请求可能调用 <code>WriteAsync</code> 数十次。如果用 <code>Task</code>,每次都分配对象;用 <code>ValueTask</code>,大部分情况无分配。</p>
<p><strong>实际效果</strong>:</p>
<ul>
<li>单个请求节省数百字节的分配</li>
<li>高并发场景(10,000 请求/秒):节省 MB 级别的分配</li>
<li>GC 压力显著降低</li>
</ul>
<hr>
<h4 id="案例-3socketreceiveasync">案例 3:Socket.ReceiveAsync</h4>
<p><strong>源码位置</strong>:<code>System.Net.Sockets.Socket</code></p>
<pre><code class="language-csharp">// .NET 5+ (简化)
public ValueTask<int> ReceiveAsync(Memory<byte> buffer, SocketFlags socketFlags, CancellationToken cancellationToken = default)
{
// 1. 如果接收缓冲区有数据,同步返回
if (_receiveBuffer.Available > 0)
{
int bytesRead = _receiveBuffer.Read(buffer.Span);
return new ValueTask<int>(bytesRead); // ✅ 无分配
}
// 2. 缓冲区为空,异步等待数据
return ReceiveAsyncCore(buffer, socketFlags, cancellationToken);
}
</code></pre>
<p><strong>为什么用 ValueTask?</strong></p>
<p>网络数据包通常是小块的(几十到几百字节),如果每次都分配 Task,GC 压力会非常大。</p>
<hr>
<h3 id="44-何时使用-valuetask决策树">4.4 何时使用 ValueTask?(决策树)</h3>
<p>根据 .NET 团队的指导和源码实践,我们可以总结出以下决策树:</p>
<div class="mermaid">flowchart TD
Start([需要异步方法]) --> Q1{"高频调用?<br>每秒 > 1000 次"}
Q1 -->|否| UseTask[使用 Task]
Q1 -->|是| Q2{"同步完成率?"}
Q2 -->|小于 50%| UseTask
Q2 -->|大于等于 50%| Q3{"是库代码?"}
Q3 -->|否 应用代码| UseTask2["使用 Task<br>避免过度优化"]
Q3 -->|是 库代码| UseValueTask["使用 ValueTask"]
UseValueTask --> Rules[遵守使用限制]
style Start fill:#bbdefb
style UseTask fill:#c8e6c9
style UseTask2 fill:#fff9c4
style UseValueTask fill:#a5d6a7
</div><p><strong>使用 ValueTask 的条件</strong>:</p>
<ul>
<li>✅ 高频调用(每秒 > 1000 次)</li>
<li>✅ 同步完成率高(>= 50%)</li>
<li>✅ 库代码或高性能场景</li>
</ul>
<p><strong>使用 Task 的场景</strong>:</p>
<ul>
<li>✅ 普通应用代码</li>
<li>✅ 低频调用</li>
<li>✅ 异步完成为主</li>
</ul>
<hr>
<h3 id="45-valuetask-的使用限制️-重要">4.5 ValueTask 的使用限制(⚠️ 重要)</h3>
<p>ValueTask 虽然高效,但有严格的使用限制:</p>
<h4 id="限制-1只能-await-一次">限制 1:只能 await 一次</h4>
<pre><code class="language-csharp">// ❌ 错误:多次 await
ValueTask<int> task = GetValueAsync();
int result1 = await task;
int result2 = await task; // 💥 未定义行为!
</code></pre>
<p><strong>原因</strong>:ValueTask 可能复用内部状态(如池化的 Task),第二次 await 可能拿到错误的结果。</p>
<h4 id="限制-2不能同时-await">限制 2:不能同时 await</h4>
<pre><code class="language-csharp">// ❌ 错误:多个并发 await
ValueTask<int> task = GetValueAsync();
Task.Run(async () => await task);
Task.Run(async () => await task); // 💥 竞态条件!
</code></pre>
<h4 id="限制-3不能阻塞获取结果">限制 3:不能阻塞获取结果</h4>
<pre><code class="language-csharp">// ❌ 错误:同步阻塞
ValueTask<int> task = GetValueAsync();
int result = task.Result; // 💥 可能抛出异常或死锁
</code></pre>
<p><strong>正确做法</strong>:</p>
<pre><code class="language-csharp">// ✅ 正确:立即 await,用完即弃
int result = await GetValueAsync();
</code></pre>
<p><strong>记忆口诀</strong>:ValueTask 是"一次性"的,就像纸巾,用完就扔,不能重复使用。</p>
<hr>
<h3 id="46-性能对比benchmark-数据">4.6 性能对比(Benchmark 数据)</h3>
<p>以下是真实的 Benchmark 数据(BenchmarkDotNet):</p>
<table>
<thead>
<tr>
<th>场景</th>
<th>Task<t></t></th>
<th>ValueTask<t></t></th>
<th>提升</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>同步完成</strong></td>
<td>45.2 ns</td>
<td>12.3 ns</td>
<td><strong>3.7x</strong></td>
</tr>
<tr>
<td><strong>异步完成</strong></td>
<td>120.5 ns</td>
<td>125.3 ns</td>
<td>-4%</td>
</tr>
<tr>
<td><strong>内存分配(同步)</strong></td>
<td>48 B</td>
<td><strong>0 B</strong></td>
<td><strong>100%</strong></td>
</tr>
<tr>
<td><strong>内存分配(异步)</strong></td>
<td>48 B</td>
<td>48 B</td>
<td>0%</td>
</tr>
</tbody>
</table>
<p><strong>结论</strong>:</p>
<ul>
<li><strong>同步路径</strong>:ValueTask 有巨大优势(3.7x 速度,0 分配)</li>
<li><strong>异步路径</strong>:性能相当(略慢 4%,可忽略)</li>
</ul>
<p><strong>适用场景</strong>:</p>
<ul>
<li>✅ 缓存命中率 >= 50%</li>
<li>✅ 高频调用(每秒 > 1000 次)</li>
<li>✅ 对 GC 压力敏感的场景</li>
</ul>
<hr>
<h3 id="47-总结valuetask-的核心价值">4.7 总结:ValueTask 的核心价值</h3>
<p><strong>核心问题</strong>:Task<t> 在高频同步完成场景下会产生大量堆分配。</t></p>
<p><strong>解决方案</strong>:ValueTask<t> 用值类型(struct)避免同步路径的堆分配。</t></p>
<p><strong>实际应用</strong>:.NET Core 核心库(Stream、Socket、ASP.NET Core)大量使用。</p>
<p><strong>使用原则</strong>:</p>
<ul>
<li>✅ 库代码 + 高频 + 高缓存命中率 → ValueTask</li>
<li>✅ 应用代码 + 普通场景 → Task(避免过度优化)</li>
</ul>
<p><strong>记住限制</strong>:</p>
<ul>
<li>⚠️ 只能 await 一次</li>
<li>⚠️ 不能并发 await</li>
<li>⚠️ 不能阻塞获取结果</li>
</ul>
<blockquote>
<p>💡 <strong>相关章节</strong>:关于 <code>SynchronizationContext</code> 和 <code>ConfigureAwait</code> 的详细内容,会在后续章节中详细讲解。</p>
</blockquote>
<hr>
<h2 id="5️⃣-常见陷阱与最佳实践">5️⃣ 常见陷阱与最佳实践</h2>
<p>现在让我们看看 async/await 的常见陷阱,以及如何避免它们。</p>
<hr>
<h3 id="51-陷阱-1async-void危险">5.1 陷阱 1:async void(危险!)</h3>
<p><strong>问题</strong>:<code>async void</code> 方法的异常无法被捕获!</p>
<pre><code class="language-csharp">// ❌ 危险:async void
public async void ProcessDataAsync()
{
await Task.Delay(100);
throw new Exception("Boom!"); // 💥 无法捕获,程序崩溃!
}
// 调用方
try
{
ProcessDataAsync(); // ⚠️ 立即返回,不等待
}
catch (Exception ex)
{
// 😱 永远不会捕获到异常!
}
</code></pre>
<p><strong>为什么危险?</strong></p>
<ol>
<li>异常会导致程序崩溃(未处理异常)</li>
<li>无法 await(调用方不知道何时完成)</li>
<li>调试困难</li>
</ol>
<p><strong>正确做法</strong>:</p>
<pre><code class="language-csharp">// ✅ 正确:返回 Task
public async Task ProcessDataAsync()
{
await Task.Delay(100);
throw new Exception("Boom!");
}
// 调用方
try
{
await ProcessDataAsync(); // ✅ 可以捕获异常
}
catch (Exception ex)
{
Console.WriteLine($"捕获到异常: {ex.Message}");
}
</code></pre>
<p><strong>唯一的例外</strong>:事件处理程序(因为事件签名要求 <code>void</code>)</p>
<pre><code class="language-csharp">// ✅ 可以接受:事件处理程序
private async void Button_Click(object sender, EventArgs e)
{
try
{
await ProcessDataAsync();
}
catch (Exception ex)
{
// ✅ 在方法内部处理异常
MessageBox.Show($"Error: {ex.Message}");
}
}
</code></pre>
<p><strong>记住</strong>:</p>
<ul>
<li>❌ 永远不要在普通方法中使用 <code>async void</code></li>
<li>✅ 事件处理程序可以用 <code>async void</code>,但必须内部处理异常</li>
</ul>
<hr>
<h3 id="52-陷阱-2过度使用-asyncawait">5.2 陷阱 2:过度使用 async/await</h3>
<p>有时候,async/await 并不是必需的,反而会增加不必要的开销。</p>
<h4 id="问题不必要的-asyncawait">问题:不必要的 async/await</h4>
<pre><code class="language-csharp">// ❌ 错误:不必要的 async/await
public async Task<string> GetDataAsync()
{
return await _httpClient.GetStringAsync("https://api.example.com");
// 只有一个 await,且在方法末尾,完全不需要 async
}
// ✅ 正确:直接返回 Task
public Task<string> GetDataAsync()
{
return _httpClient.GetStringAsync("https://api.example.com");
// 省略了状态机生成,性能更好
}
</code></pre>
<p><strong>为什么不需要 async?</strong></p>
<p>如果方法只有一个 <code>await</code>,并且在方法末尾,直接返回 Task 即可,无需 async:</p>
<ul>
<li>✅ 省略状态机生成(节省约 200 字节)</li>
<li>✅ 减少方法调用开销</li>
<li>✅ 异常栈更清晰</li>
</ul>
<p><strong>什么时候需要 async?</strong></p>
<p>只有在以下情况才需要 async:</p>
<ol>
<li>方法中有多个 await</li>
<li>需要 try-catch 包装异常</li>
<li>需要 using 语句</li>
<li>需要在 await 前后执行逻辑</li>
</ol>
<h4 id="问题在同步方法中不必要地使用-taskrun">问题:在同步方法中不必要地使用 Task.Run</h4>
<pre><code class="language-csharp">// ❌ 错误:不必要的 Task.Run
public async Task<string> GetDataAsync()
{
return await Task.Run(async () =>
{
using HttpClient client = new HttpClient();
return await client.GetStringAsync("https://api.example.com");
}); // 多此一举!浪费了一个线程
}
// ✅ 正确:I/O 操作直接 await
public async Task<string> GetDataAsync()
{
using HttpClient client = new HttpClient();
return await client.GetStringAsync("https://api.example.com");
}
</code></pre>
<p><strong>为什么错误?</strong></p>
<p>I/O 操作(如网络请求、文件读取)<strong>本身就是异步的</strong>,不需要 Task.Run:</p>
<ul>
<li>❌ Task.Run 会占用一个线程池线程等待</li>
<li>❌ 完全没必要,还浪费资源</li>
</ul>
<p><strong>什么时候用 Task.Run?</strong></p>
<p>只有在<strong>计算密集型任务</strong>需要卸载到后台线程时才用 Task.Run:</p>
<pre><code class="language-csharp">// ✅ 正确:CPU 密集型任务使用 Task.Run
public async Task<int> CalculateAsync(int[] numbers)
{
return await Task.Run(() =>
{
// 复杂的计算
return numbers.Sum(n => n * n);
});
}
</code></pre>
<hr>
<h3 id="53-陷阱-3死锁result-或-wait">5.3 陷阱 3:死锁(.Result 或 .Wait())</h3>
<p>这是第二常见的陷阱,尤其在 UI 应用和 ASP.NET Framework 中。</p>
<h4 id="死锁场景">死锁场景</h4>
<pre><code class="language-csharp">// WinForms/WPF 应用
public void Button_Click(object sender, EventArgs e)
{
// ❌ 死锁!
var data = LoadDataAsync().Result; // 阻塞 UI 线程
UpdateUI(data);
}
public async Task<string> LoadDataAsync()
{
await Task.Delay(1000); // 等待完成后,尝试回到 UI 线程
return "Data";
}
</code></pre>
<p><strong>死锁原因</strong>:</p>
<pre><code>UI 线程: [等待 Task 完成] ──阻塞──┐
│
Task: [等待 UI 线程] ────回调──┘
↑ 死锁!互相等待
</code></pre>
<p><strong>解决方案 1:使用 async/await(推荐)</strong></p>
<pre><code class="language-csharp">// ✅ 正确:使用 async/await
public async void Button_Click(object sender, EventArgs e)
{
var data = await LoadDataAsync(); // ✅ 不阻塞
UpdateUI(data);
}
</code></pre>
<p><strong>解决方案 2:使用 ConfigureAwait(false)</strong></p>
<pre><code class="language-csharp">public void Button_Click(object sender, EventArgs e)
{
var data = LoadDataAsync().Result; // ⚠️ 仍然不推荐
}
public async Task<string> LoadDataAsync()
{
// ✅ 不捕获上下文,避免死锁
await Task.Delay(1000).ConfigureAwait(false);
return "Data";
}
</code></pre>
<blockquote>
<p>📝 <strong>注意</strong>:关于 <code>ConfigureAwait</code> 的详细内容,会在后续章节详细讲解。</p>
</blockquote>
<p><strong>最佳实践</strong>:</p>
<ul>
<li>✅ <strong>优先使用 async/await</strong>("一路 async 到底")</li>
<li>❌ <strong>避免同步等待异步方法</strong>(<code>.Result</code> 或 <code>.Wait()</code>)</li>
<li>⚠️ <strong>如果实在需要</strong>:在异步方法中使用 <code>ConfigureAwait(false)</code></li>
</ul>
<hr>
<h3 id="54-最佳实践总结">5.4 最佳实践总结</h3>
<p><strong>DO(推荐做法)</strong>:</p>
<ul>
<li>✅ 返回 <code>Task</code> 或 <code>Task<T></code>,避免 <code>async void</code>(事件处理除外)</li>
<li>✅ "一路 async 到底"(Async All the Way)</li>
<li>✅ 方法名以 <code>Async</code> 结尾</li>
<li>✅ 使用 <code>CancellationToken</code> 支持取消(后续章节会讲)</li>
<li>✅ I/O 操作使用异步 API(不要用 Task.Run 包装)</li>
<li>✅ 高频调用考虑使用 <code>ValueTask<T></code>(库代码)</li>
</ul>
<p><strong>DON'T(避免做法)</strong>:</p>
<ul>
<li>❌ 不要使用 <code>async void</code>(除了事件处理程序)</li>
<li>❌ 不要使用 <code>.Result</code> 或 <code>.Wait()</code></li>
<li>❌ 不要在 I/O 操作上使用 <code>Task.Run</code></li>
<li>❌ 不要过度 await(方法末尾的单个 await 可以省略)</li>
</ul>
<p><strong>性能优化</strong>:</p>
<ul>
<li>⚡ 高频调用 + 高缓存命中率 → 使用 <code>ValueTask<T></code></li>
<li>⚡ 避免不必要的 async(方法末尾的单个 await)</li>
<li>⚡ 库代码考虑使用 <code>ConfigureAwait(false)</code></li>
</ul>
<hr>
<h2 id="6️⃣-asyncawait-的设计问题与未来演进">6️⃣ async/await 的设计问题与未来演进</h2>
<p>async/await 虽然强大,但并非完美。让我们客观地看看它的局限性和未来的改进方向。</p>
<hr>
<h3 id="61-async-的传染性the-async-infection">6.1 async 的传染性(The Async Infection)</h3>
<p>这是 async/await 最大的设计问题之一。</p>
<h4 id="问题描述">问题描述</h4>
<p>一旦你的方法变成 <code>async</code>,所有调用它的方法也必须变成 <code>async</code>,形成"传染"。</p>
<pre><code class="language-csharp">// 第 1 层:数据访问层
public async Task<User> GetUserAsync(int id)
{
return await _database.QueryAsync<User>("SELECT * FROM Users WHERE Id = @id", new { id });
}
// 第 2 层:业务逻辑层(被迫 async)
public async Task<UserDto> GetUserDtoAsync(int id)
{
var user = await GetUserAsync(id); // ⚠️ 必须 await
return MapToDto(user);
}
// 第 3 层:控制器(被迫 async)
public async Task<IActionResult> GetUser(int id)
{
var dto = await GetUserDtoAsync(id); // ⚠️ 必须 await
return Ok(dto);
}
</code></pre>
<p><strong>传染路径</strong>:</p>
<pre><code>GetUserAsync (async)
↓ 传染
GetUserDtoAsync (被迫 async)
↓ 传染
GetUser (被迫 async)
↓ 传染
整个调用链都是 async
</code></pre>
<h4 id="为什么是问题">为什么是问题?</h4>
<ol>
<li><strong>无法混合同步和异步代码</strong></li>
</ol>
<pre><code class="language-csharp">// ❌ 无法在同步方法中优雅地调用异步方法
public UserDto GetUserDto(int id)
{
// 方式 1:阻塞(死锁风险)
var dto = GetUserDtoAsync(id).Result; // 💥 可能死锁
// 方式 2:转同步(丑陋)
var dto = GetUserDtoAsync(id).GetAwaiter().GetResult(); // 😱 丑陋
// 方式 3:改成 async(传染)
// 无法实现,因为调用方可能要求同步接口
}
</code></pre>
<ol start="2">
<li><strong>接口兼容性问题</strong></li>
</ol>
<pre><code class="language-csharp">// 现有同步接口
public interface IUserService
{
User GetUser(int id);
}
// 想改成异步?必须改接口(破坏性变更)
public interface IUserService
{
Task<User> GetUserAsync(int id); // ⚠️ 破坏现有实现
}
</code></pre>
<ol start="3">
<li><strong>库设计困境</strong></li>
</ol>
<p>库作者必须提供两套 API:</p>
<pre><code class="language-csharp">// Json.NET 的困境
public class JsonConvert
{
public static string SerializeObject(object value); // 同步版本
public static Task<string> SerializeObjectAsync(object value); // 异步版本
// 维护两套代码,痛苦!
}
</code></pre>
<hr>
<h3 id="62-asyncawait-的性能开销">6.2 async/await 的性能开销</h3>
<p>虽然 async/await 比手动写回调好得多,但仍有性能开销。</p>
<h4 id="状态机开销">状态机开销</h4>
<p>每个 async 方法都会生成一个状态机:</p>
<pre><code class="language-csharp">// 简单的 async 方法
public async Task<int> GetValueAsync()
{
await Task.Delay(100);
return 42;
}
// 编译器生成的状态机(简化)
struct GetValueAsyncStateMachine
{
public int State;
public AsyncTaskMethodBuilder<int> Builder;
public TaskAwaiter Awaiter;
// ... 约 200+ 字节的开销
}
</code></pre>
<p><strong>开销</strong>:</p>
<ul>
<li>状态机结构体:约 200 字节</li>
<li>AsyncTaskMethodBuilder:额外开销</li>
<li>装箱(如果状态机需要堆分配)</li>
</ul>
<hr>
<h3 id="63-runtime-async下一代异步模型">6.3 Runtime Async:下一代异步模型</h3>
<p>微软在 2019 年提出了 <strong>async2</strong> 项目(后改名为 <strong>Runtime Async</strong>),旨在解决 async/await 的设计缺陷。</p>
<h4 id="核心目标">核心目标</h4>
<ol>
<li><strong>消除 async 传染性</strong>:允许同步和异步代码无缝互操作</li>
<li><strong>零开销抽象</strong>:async 方法的性能接近普通方法</li>
<li><strong>向后兼容</strong>:不破坏现有代码</li>
</ol>
<h4 id="当前状态">当前状态</h4>
<p>截至目前,Runtime Async 仍在设计和实验阶段,可能在 .NET 11 或更高版本中引入。</p>
<p><strong>官方资源</strong>:</p>
<ul>
<li>[.NET Runtime-Async Feature](.NET Runtime-Async Feature · Issue #109632 · dotnet/runtime)</li>
</ul>
<hr>
<h3 id="64-当前的应对策略">6.4 当前的应对策略</h3>
<p>在 Runtime Async 正式发布之前,我们可以这样应对:</p>
<h4 id="1-接受-async-传染性">1. 接受 async 传染性</h4>
<pre><code class="language-csharp">// ✅ 正确:一路 async 到底
public async Task<IActionResult> GetUser(int id)
{
var user = await _userService.GetUserAsync(id);
return Ok(user);
}
</code></pre>
<p><strong>原则</strong>:"Async all the way"(一路异步到底)</p>
<h4 id="2-提供同步和异步两套-api库代码">2. 提供同步和异步两套 API(库代码)</h4>
<pre><code class="language-csharp">// 库代码的标准做法
public class MyService
{
// 同步版本
public User GetUser(int id)
{
// 实现...
}
// 异步版本
public async Task<User> GetUserAsync(int id)
{
// 实现...
}
}
</code></pre>
<h4 id="3-使用-valuetask-减少开销">3. 使用 ValueTask 减少开销</h4>
<pre><code class="language-csharp">// ✅ 高频调用,使用 ValueTask
public ValueTask<int> GetCachedValueAsync(int key)
{
if (_cache.TryGetValue(key, out int value))
return new ValueTask<int>(value); // 无分配
return new ValueTask<int>(_database.GetAsync(key));
}
</code></pre>
<hr>
<h3 id="65-总结asyncawait-的现状与未来">6.5 总结:async/await 的现状与未来</h3>
<p><strong>现状</strong>:</p>
<ul>
<li>✅ async/await 是目前最好的异步模型</li>
<li>⚠️ 但有传染性、性能开销等问题</li>
<li>⚠️ 需要遵守最佳实践,避免常见陷阱</li>
</ul>
<p><strong>未来</strong>:</p>
<ul>
<li>🚀 Runtime Async 旨在解决这些问题</li>
<li>🚀 零开销、无传染性、向后兼容</li>
<li>⏳ 但何时发布尚不明确</li>
</ul>
<p><strong>建议</strong>:</p>
<ul>
<li>✅ 继续使用 async/await,它仍是最佳选择</li>
<li>✅ 遵守最佳实践(本章讲过的)</li>
<li>✅ 关注 Runtime Async 的进展</li>
</ul>
<hr>
<h2 id="7️⃣-章节总结">7️⃣ 章节总结</h2>
<h3 id="本章回顾">本章回顾</h3>
<p>在本章中,我们深入探讨了 async/await 的方方面面:</p>
<h4 id="-核心知识点">🎯 核心知识点</h4>
<ol>
<li>
<p><strong>为什么需要 async/await</strong>(第 0 章)</p>
<ul>
<li>回调地狱的痛苦(Thread、APM)</li>
<li>async/await 的三大价值:消灭回调、自动上下文管理、高效 I/O</li>
</ul>
</li>
<li>
<p><strong>编译器魔法</strong>(第 2 章)</p>
<ul>
<li>状态机的生成和工作原理</li>
<li><code>AsyncTaskMethodBuilder</code> 的作用</li>
<li>await 的本质(ContinueWith + 状态切换)</li>
</ul>
</li>
<li>
<p><strong>线程真相</strong>(第 3 章)</p>
<ul>
<li>async/await ≠ 多线程</li>
<li>I/O 完成端口(IOCP)的作用</li>
<li>线程释放和恢复机制</li>
</ul>
</li>
<li>
<p><strong>性能优化:ValueTask</strong>(第 4 章)</p>
<ul>
<li>Task<t> 的堆分配问题</t></li>
<li>ValueTask<t> 的值类型优势</t></li>
<li>.NET Core 源码实战(Stream、ASP.NET Core、Socket)</li>
<li>决策树和使用限制</li>
</ul>
</li>
<li>
<p><strong>常见陷阱</strong>(第 5 章)</p>
<ul>
<li>async void 的危险</li>
<li>过度使用 async/await</li>
<li>死锁(.Result/.Wait())</li>
<li>最佳实践</li>
</ul>
</li>
<li>
<p><strong>设计问题与未来</strong>(第 6 章)</p>
<ul>
<li>async 的传染性</li>
<li>性能开销</li>
<li>Runtime Async 的未来</li>
</ul>
</li>
</ol>
<hr>
<h3 id="核心要点总结">核心要点总结</h3>
<h4 id="-do推荐做法">✅ DO(推荐做法)</h4>
<table>
<thead>
<tr>
<th>类别</th>
<th>做法</th>
<th>原因</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>返回类型</strong></td>
<td>使用 <code>Task<T></code> 或 <code>ValueTask<T></code></td>
<td>可以 await,异常可捕获</td>
</tr>
<tr>
<td><strong>方法命名</strong></td>
<td>以 <code>Async</code> 结尾</td>
<td>符合约定,易于识别</td>
</tr>
<tr>
<td><strong>I/O 操作</strong></td>
<td>直接 <code>await</code> 异步 API</td>
<td>不占用线程,高效</td>
</tr>
<tr>
<td><strong>CPU 密集</strong></td>
<td><code>await Task.Run(...)</code></td>
<td>卸载到后台线程</td>
</tr>
<tr>
<td><strong>异常处理</strong></td>
<td>try-catch 包裹 await</td>
<td>优雅处理异常</td>
</tr>
<tr>
<td><strong>高频调用</strong></td>
<td>考虑 <code>ValueTask<T></code></td>
<td>减少 GC 压力(库代码)</td>
</tr>
<tr>
<td><strong>一路 async</strong></td>
<td>Async All the Way</td>
<td>避免死锁</td>
</tr>
</tbody>
</table>
<h4 id="-dont避免做法">❌ DON'T(避免做法)</h4>
<table>
<thead>
<tr>
<th>陷阱</th>
<th>问题</th>
<th>后果</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>async void</strong></td>
<td>异常无法捕获</td>
<td>应用崩溃 💀</td>
</tr>
<tr>
<td><strong>.Result / .Wait()</strong></td>
<td>阻塞线程</td>
<td>死锁风险 💀</td>
</tr>
<tr>
<td><strong>不必要的 async</strong></td>
<td>状态机开销</td>
<td>性能损失</td>
</tr>
<tr>
<td><strong>Task.Run 包装 I/O</strong></td>
<td>浪费线程</td>
<td>资源浪费</td>
</tr>
<tr>
<td><strong>忘记 await</strong></td>
<td>异常被吞掉</td>
<td>难以调试</td>
</tr>
<tr>
<td><strong>多次 await ValueTask</strong></td>
<td>未定义行为</td>
<td>结果错误 💥</td>
</tr>
</tbody>
</table>
<hr>
<h3 id="性能优化清单">性能优化清单</h3>
<h4 id="-高性能场景">🚀 高性能场景</h4>
<p>如果你的代码属于以下场景,应该特别关注性能优化:</p>
<ul>
<li>✅ <strong>高并发 Web API</strong>(每秒 > 1000 请求)</li>
<li>✅ <strong>实时游戏服务器</strong>(低延迟要求)</li>
<li>✅ <strong>金融交易系统</strong>(极致性能)</li>
<li>✅ <strong>库代码</strong>(被大量调用)</li>
</ul>
<p><strong>优化手段</strong>:</p>
<table>
<thead>
<tr>
<th>优化</th>
<th>场景</th>
<th>效果</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>ValueTask<t></t></strong></td>
<td>高频 + 高缓存命中率</td>
<td>减少 GC 压力(3.7x 提升)</td>
</tr>
<tr>
<td><strong>ConfigureAwait(false)</strong></td>
<td>库代码</td>
<td>避免上下文切换</td>
</tr>
<tr>
<td><strong>避免不必要的 async</strong></td>
<td>方法末尾单个 await</td>
<td>省略状态机开销</td>
</tr>
<tr>
<td><strong>缓存 Task</strong></td>
<td>常量结果</td>
<td>避免重复分配</td>
</tr>
</tbody>
</table>
<h4 id="-性能数据回顾">📊 性能数据回顾</h4>
<p><strong>ValueTask vs Task</strong>(同步完成场景):</p>
<ul>
<li>速度:12.3 ns vs 45.2 ns(<strong>3.7x 提升</strong>)</li>
<li>内存:0 B vs 48 B(<strong>零分配</strong>)</li>
</ul>
<p><strong>适用条件</strong>:</p>
<ul>
<li>高频调用(每秒 > 1000 次)</li>
<li>同步完成率 >= 50%</li>
<li>库代码或高性能场景</li>
</ul>
<hr>
<h3 id="常见问题-faq">常见问题 FAQ</h3>
<h4 id="q1什么时候用-valuetask">Q1:什么时候用 ValueTask?</h4>
<p><strong>A</strong>:高频调用(每秒 > 1000 次)+ 高缓存命中率(>= 50%)+ 库代码。</p>
<p><strong>判断标准</strong>:</p>
<ul>
<li>✅ 使用场景:库代码、高性能 API、游戏服务器</li>
<li>✅ 调用频率:每秒 > 1000 次</li>
<li>✅ 缓存命中率:>= 50%(同步完成)</li>
<li>❌ 普通应用代码:用 Task<t> 即可(避免过度优化)</t></li>
</ul>
<p><strong>记忆口诀</strong>:<strong>高频库代码,缓存命中高,ValueTask 才考虑。</strong></p>
<hr>
<h4 id="q2async-void-什么时候能用">Q2:async void 什么时候能用?</h4>
<p><strong>A</strong>:只有事件处理程序可以用,且必须内部处理异常。</p>
<p><strong>唯一例外</strong>:</p>
<pre><code class="language-csharp">// ✅ 事件处理程序:可以用 async void
private async void Button_Click(object sender, EventArgs e)
{
try
{
await ProcessDataAsync();
}
catch (Exception ex)
{
// ✅ 必须内部处理异常
MessageBox.Show($"Error: {ex.Message}");
}
}
</code></pre>
<p><strong>其他场景</strong>:一律使用 <code>async Task</code>。</p>
<p><strong>原因</strong>:</p>
<ul>
<li>❌ async void 的异常无法被调用方捕获</li>
<li>❌ 无法 await(调用方不知道何时完成)</li>
<li>❌ 异常会导致应用崩溃 💀</li>
</ul>
<hr>
<h4 id="q3如何避免死锁">Q3:如何避免死锁?</h4>
<p><strong>A</strong>:最佳方案是"一路 async 到底"(Async All the Way)。</p>
<p><strong>三种方案对比</strong>:</p>
<table>
<thead>
<tr>
<th>方案</th>
<th>适用场景</th>
<th>优先级</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>一路 async</strong></td>
<td>所有场景</td>
<td>⭐⭐⭐⭐⭐ 首选</td>
</tr>
<tr>
<td><strong>ConfigureAwait(false)</strong></td>
<td>库代码</td>
<td>⭐⭐⭐⭐ 备选</td>
</tr>
<tr>
<td><strong>同步方法</strong></td>
<td>不得已</td>
<td>⭐ 避免使用</td>
</tr>
</tbody>
</table>
<p><strong>示例</strong>:</p>
<pre><code class="language-csharp">// ✅ 方案 1:一路 async(推荐)
public async Task LoadDataAsync()
{
var data = await GetDataAsync();
UpdateUI(data);
}
// ✅ 方案 2:库代码使用 ConfigureAwait(false)
public async Task<string> GetDataAsync()
{
return await _httpClient.GetStringAsync("...")
.ConfigureAwait(false); // 不捕获上下文
}
</code></pre>
<p><strong>永远不要</strong>:</p>
<pre><code class="language-csharp">// ❌ 错误:同步等待异步方法
var data = GetDataAsync().Result; // 💥 死锁!
</code></pre>
<hr>
<h4 id="q4io-操作需要-taskrun-吗">Q4:I/O 操作需要 Task.Run 吗?</h4>
<p><strong>A</strong>:不需要!I/O 操作本身就是异步的,直接 await 即可。</p>
<p><strong>正确做法</strong>:</p>
<pre><code class="language-csharp">// ✅ 正确:I/O 操作直接 await
public async Task<string> ReadFileAsync(string path)
{
return await File.ReadAllTextAsync(path);
// I/O 操作不占用线程,使用 I/O 完成端口
}
</code></pre>
<p><strong>错误做法</strong>:</p>
<pre><code class="language-csharp">// ❌ 错误:I/O 操作用 Task.Run
public async Task<string> ReadFileAsync(string path)
{
return await Task.Run(async () =>
{
return await File.ReadAllTextAsync(path);
// 💥 多此一举!浪费一个线程池线程
});
}
</code></pre>
<p><strong>Task.Run 的正确用法</strong>:只用于 CPU 密集型任务</p>
<pre><code class="language-csharp">// ✅ 正确:CPU 密集型任务使用 Task.Run
public async Task<int> CalculateSumAsync(int[] numbers)
{
return await Task.Run(() =>
{
return numbers.Sum(n => n * n); // 复杂计算
});
}
</code></pre>
<hr>
<h4 id="q5async-方法的性能开销有多大">Q5:async 方法的性能开销有多大?</h4>
<p><strong>A</strong>:约 200 字节的状态机结构体 + AsyncTaskMethodBuilder 开销。对于 I/O 操作,这点开销可以忽略。</p>
<p><strong>性能数据</strong>:</p>
<ul>
<li>状态机结构体:约 200 字节</li>
<li>异步路径开销:约 120 ns(相比同步多 100-200 ns)</li>
<li>同步路径开销:可以优化到几乎为零(IsCompleted == true)</li>
</ul>
<p><strong>是否需要担心?</strong></p>
<table>
<thead>
<tr>
<th>场景</th>
<th>开销是否重要</th>
<th>建议</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>I/O 操作</strong></td>
<td>❌ 不重要</td>
<td>放心使用 async</td>
</tr>
<tr>
<td><strong>网络请求</strong></td>
<td>❌ 不重要</td>
<td>放心使用 async</td>
</tr>
<tr>
<td><strong>文件读写</strong></td>
<td>❌ 不重要</td>
<td>放心使用 async</td>
</tr>
<tr>
<td><strong>高频同步方法</strong></td>
<td>✅ 重要</td>
<td>考虑 ValueTask 或直接返回 Task</td>
</tr>
<tr>
<td><strong>纯计算</strong></td>
<td>✅ 重要</td>
<td>不要用 async</td>
</tr>
</tbody>
</table>
<p><strong>记忆口诀</strong>:I/O 操作用 async,性能开销可忽略;高频同步方法,考虑 ValueTask 或直接返回 Task。</p>
<hr>
<h3 id="参考资源">参考资源</h3>
<h4 id="-官方文档">📚 官方文档</h4>
<ol>
<li>
<p><strong>异步编程模式(Microsoft Learn)</strong></p>
<ul>
<li>🔗 https://learn.microsoft.com/zh-cn/dotnet/csharp/asynchronous-programming/</li>
<li>Microsoft 官方的 async/await 完整指南</li>
<li>包括基础概念、最佳实践、性能优化</li>
</ul>
</li>
<li>
<p><strong>Task-based Asynchronous Pattern (TAP)</strong></p>
<ul>
<li>🔗 https://learn.microsoft.com/en-us/dotnet/standard/asynchronous-programming-patterns/task-based-asynchronous-pattern-tap</li>
<li>TAP 的详细规范</li>
<li>包括命名约定、返回类型、取消和进度报告</li>
</ul>
</li>
<li>
<p><strong>ValueTask 文档</strong></p>
<ul>
<li>🔗 https://learn.microsoft.com/zh-cn/dotnet/api/system.threading.tasks.valuetask-1</li>
<li>官方 API 文档</li>
<li>包括使用限制和最佳实践</li>
</ul>
</li>
</ol>
<hr>
<h4 id="-net-官方源码">🎯 .NET 官方源码</h4>
<ol start="4">
<li>
<p><strong>AsyncTaskMethodBuilder 源码</strong></p>
<ul>
<li>🔗 https://source.dot.net/#System.Private.CoreLib/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncTaskMethodBuilder.cs</li>
<li>查看编译器生成的状态机如何工作</li>
<li>理解 Task 的生命周期管理</li>
</ul>
</li>
<li>
<p><strong>SynchronizationContext 源码</strong></p>
<ul>
<li>🔗 https://source.dot.net/#System.Private.CoreLib/src/libraries/System.Private.CoreLib/src/System/Threading/SynchronizationContext.cs</li>
<li>理解上下文捕获机制</li>
<li>查看不同框架的上下文实现</li>
</ul>
</li>
<li>
<p><strong>Task 源码</strong></p>
<ul>
<li>🔗 https://source.dot.net/#System.Private.CoreLib/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.cs</li>
<li>Task 的完整实现</li>
<li>包括状态管理、延续链、异常处理</li>
</ul>
</li>
<li>
<p><strong>ValueTask 源码</strong></p>
<ul>
<li>🔗 https://source.dot.net/#System.Private.CoreLib/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/ValueTask.cs</li>
<li>ValueTask 的完整实现</li>
<li>理解值类型如何避免堆分配</li>
</ul>
</li>
</ol>
<hr>
<h4 id="-net-团队博客必读">🌟 .NET 团队博客(必读!)</h4>
<ol start="8">
<li>
<p><strong>Stephen Toub 系列文章</strong></p>
<ul>
<li>🔗 Stephen Toub - MSFT, Author at .NET Blog</li>
<li><strong>推荐文章</strong>:
<ul>
<li>ConfigureAwait FAQ ⭐⭐⭐⭐⭐</li>
<li>Understanding ValueTask ⭐⭐⭐⭐⭐</li>
<li>How Async/Await Really Works in C# ⭐⭐⭐⭐⭐</li>
</ul>
</li>
</ul>
</li>
<li>
<p><strong>David Fowler 的异步指南</strong></p>
<ul>
<li>https://github.com/davidfowl/AspNetCoreDiagnosticScenarios/blob/master/AsyncGuidance.md</li>
<li>ASP.NET Core 架构师的最佳实践</li>
<li>包括常见陷阱、性能优化、诊断技巧</li>
</ul>
</li>
<li>
<p><strong>Async/Await Best Practices</strong></p>
<ul>
<li>Async/Await - Best Practices in Asynchronous Programming</li>
<li>Stephen Cleary(异步编程专家)的经典文章</li>
<li>MSDN Magazine 2013 年 3 月刊</li>
</ul>
</li>
</ol>
<hr>
<h4 id="️-工具">🛠️ 工具</h4>
<ol start="13">
<li>
<p><strong>SharpLab</strong></p>
<ul>
<li>🔗 https://sharplab.io/</li>
<li>在线查看 C# 代码编译后的 IL 代码和状态机</li>
<li>支持 C# to C#、C# to IL、C# to ASM</li>
</ul>
</li>
<li>
<p><strong>BenchmarkDotNet</strong></p>
<ul>
<li>🔗 https://benchmarkdotnet.org/</li>
<li>用于性能测试的专业工具</li>
<li>支持内存分配、GC、CPU 指令等多维度测量</li>
</ul>
</li>
<li>
<p><strong>PerfView</strong></p>
<ul>
<li>🔗 https://github.com/microsoft/perfview</li>
<li>Microsoft 的性能分析工具</li>
<li>支持 CPU、内存、GC、异步操作的分析</li>
</ul>
</li>
</ol>
<hr>
<blockquote>
<p>💡 <strong>全文完</strong>:感谢阅读!如果有疑问,欢迎在评论区讨论。</p>
<p>🎓 <strong>下一步</strong>:准备好了吗?下一章节,我们将详细讲一讲SynchronizationContext 与死锁的问题。</p>
</blockquote><br><br>
来源:https://www.cnblogs.com/diamondhusky/p/19934031
頁:
[1]