Day 1 - 异步基础
建议用时:240-300 分钟
你将学会什么
- 同步阻塞和异步等待有什么区别
Task为什么不是结果本身,而是“未来会完成的工作”async和await分别负责什么Task、Task<T>、void在异步方法里怎么选择- 异步异常应该怎么捕获
- 文件读取这类真实等待场景怎么写异步代码
异步不是让所有代码都变快。异步主要解决“等待期间不要把线程卡死”的问题,尤其是文件、网络、数据库、接口请求这些需要等待外部结果的场景。
本页固定顺序
- 先学第一部分:弄懂今天最小、最重要的知识,并运行短例子。
- 再学第二部分:把刚学的知识组合成一个完整例子。
- 然后做第三部分:自己跟着敲,再完成重复训练和每日小测。
- 最后做第四部分:先独立完成作业,再用完整答案检查。
学习衔接
上一页学习的是“第三周复盘与综合练习”,今天继续学习“异步基础”。先使用上一页已经会的写法,再只增加今天这个新知识点;如果前置内容还不能独立敲出,先回上一页复习,不要硬跳。
今天的最低通过线
第一次学习不要求背完整页。完成下面 3 项,就可以继续:
- 能用自己的话说明“异步基础”解决什么问题。
- 把第一部分的短例子亲手敲完,并确认每个例子都能运行。
- 不看完整答案完成第三部分至少前 3 个例子,再主动改一个值观察结果。
第一部分:先学原理和最小知识
这一部分从最小知识开始。先读解释,再把紧跟着的短例子敲一遍。异步容易混乱,是因为很多人把 Task、线程、等待、并发混在一起看。
1. 同步阻塞是什么
同步阻塞就是:当前线程必须一直等,等完才能继续往下执行。
例如:
开始做饭
等水烧开
水烧开后下面
面煮好后吃饭如果“等水烧开”的时候什么都不做,就是同步阻塞。
在代码里,像 Thread.Sleep(1000) 这种写法会让当前线程硬等 1 秒。
2. 异步等待是什么
异步等待就是:这个方法需要等结果,但等待期间不死占着当前执行机会。
可以这样理解:
开始烧水
把“水烧开后继续”这件事记下来
等待期间先不堵在原地
水烧开后继续下面await 的意思不是“开一个新线程”,而是:
等这个 Task 完成,完成后从这里继续。3. Task 是什么
Task 表示一个将来会完成的工作。
它不是结果本身。
| 写法 | 含义 |
|---|---|
Task | 将来会完成,但没有返回值 |
Task<int> | 将来会完成,并返回一个 int |
Task<string> | 将来会完成,并返回一个 string |
Task<List<Order>> | 将来会完成,并返回订单列表 |
如果你看到:
Task<int>读法应该是:
一个将来会得到 int 的任务4. async 是什么
async 是写在方法前面的标记,表示这个方法里面可以使用 await。
例如:
async Task LoadAsync()注意:async 本身不代表一定开线程,也不代表一定变快。
它只是告诉编译器:
这个方法里可能会 await 一些异步任务。5. await 是什么
await 是等待一个 Task 完成。
例如:
string text = await File.ReadAllTextAsync(path)读法:
等待文件读取完成,然后把读取结果放进 text。如果方法返回 Task<int>,await 之后得到 int。
如果方法返回 Task<string>,await 之后得到 string。
如果方法返回 Task,await 之后没有返回值,只表示等它完成。
6. Task 和 Task<T> 怎么选
| 方法要做什么 | 返回类型 |
|---|---|
| 异步保存文件,不需要返回结果 | Task |
异步读取数量,需要返回 int | Task<int> |
异步读取文本,需要返回 string | Task<string> |
| 异步读取订单列表 | Task<List<Order>> |
不要把 async void 用在普通业务方法里。
普通异步方法优先写:
async Task
async Task<T>async void 主要出现在事件处理方法里。
7. 异步和线程不是一回事
异步经常和线程一起出现,但它们不是同一个概念。
| 概念 | 关注点 |
|---|---|
| 异步 | 等待期间怎么继续安排后续代码 |
| 线程 | CPU 执行代码的通道 |
| 并发 | 多件事在同一段时间内推进 |
| 并行 | 多件事真的同时在多个核心上执行 |
文件、网络、数据库这类等待,优先使用异步 API。
大量 CPU 计算,不是简单加 async 就会变快。
8. C# 的 async/await 和协程的关系
有些语言会把类似能力叫协程。
你可以先这样理解:
遇到 await,当前异步方法暂时停在这里。
等 Task 完成后,再从 await 后面继续执行。这和“协程让出执行权,后面再恢复”的思想相似。
但在 C# 里你先记住实际写法:
方法返回 Task 或 Task<T>
调用处使用 await9. 为什么不要随便用 .Result 和 .Wait()
await 是异步等待。
.Result 和 .Wait() 是同步阻塞。
如果在异步代码里乱用 .Result 或 .Wait(),可能导致:
- 当前线程被堵住。
- 程序响应变慢。
- 某些环境下出现死锁。
- 异常信息更难读。
所以本课程先定一个规则:
能 await,就不要 .Result 或 .Wait()。10. 异步异常怎么处理
异步方法里抛出的异常,会在 await 那一行被重新抛出来。
所以异常处理仍然写:
try
{
await SomeAsync();
}
catch (Exception ex)
{
...
}不要以为异步异常就不能抓。只要你 await 了,就可以正常用 try/catch。
第二部分:把知识组合成完整例子
前面已经学过最小知识。现在把它们组合起来,先读懂执行顺序,再完整敲一遍。今天最终要能写出一个异步方法,并在调用处用 await 等它完成。
先看效果:异步读取文件
string path = "orders.txt";
await File.WriteAllTextAsync(path, "SO-001\nSO-002\nSO-003");
async Task<List<string>> LoadOrdersAsync(string filePath)
{
Console.WriteLine("开始读取订单文件");
string text = await File.ReadAllTextAsync(filePath);
List<string> result = text
.Split('\n', StringSplitOptions.RemoveEmptyEntries)
.ToList();
Console.WriteLine("订单文件读取完成");
return result;
}
List<string> orders = await LoadOrdersAsync(path);
Console.WriteLine($"读取到 {orders.Count} 个订单");
foreach (string order in orders)
{
Console.WriteLine(order);
}
这里最重要的是这两句:
async Task<List<string>> LoadOrdersAsync(...)List<string> orders = await LoadOrdersAsync(path);第一句表示:这个方法是异步方法,将来会返回一个 List<string>。
第二句表示:调用这个异步方法,等待它完成,并拿到真正的结果。
第三部分:跟着敲代码
从这里开始动手。每个例子都是完整代码,可以直接放进 Program.cs 运行。
动手前先做这 3 件事
- 打开一个控制台项目。
- 每次只保留一个例子的代码,运行通过后再换下一个。
- 每个例子运行后,改一次等待时间、返回值或文件内容,再运行观察结果。
例子 1:同步阻塞等待
using System.Diagnostics;
Stopwatch stopwatch = Stopwatch.StartNew();
Console.WriteLine("开始同步等待");
Thread.Sleep(1000);
Console.WriteLine("同步等待结束");
stopwatch.Stop();
Console.WriteLine($"耗时: {stopwatch.ElapsedMilliseconds} 毫秒");Thread.Sleep 会硬等。它适合说明阻塞,不适合模拟真实异步业务。
例子 2:最小异步等待
using System.Diagnostics;
Stopwatch stopwatch = Stopwatch.StartNew();
Console.WriteLine("开始异步等待");
await Task.Delay(1000);
Console.WriteLine("异步等待结束");
stopwatch.Stop();
Console.WriteLine($"耗时: {stopwatch.ElapsedMilliseconds} 毫秒");Task.Delay 表示一个异步等待任务,await 等它完成。
例子 3:把异步等待放进方法
async Task LoadAsync()
{
Console.WriteLine("开始加载");
await Task.Delay(1000);
Console.WriteLine("加载完成");
}
await LoadAsync();
LoadAsync 返回 Task,表示它是一个会异步完成的方法,但没有返回值。
例子 4:异步方法返回数字
async Task<int> GetOrderCountAsync()
{
Console.WriteLine("开始统计订单数量");
await Task.Delay(500);
return 3;
}
int count = await GetOrderCountAsync();
Console.WriteLine($"订单数量: {count}");
Task<int> 表示将来会得到一个 int。
await GetOrderCountAsync() 之后,拿到的才是 int count。
例子 5:异步方法返回字符串
async Task<string> GetCustomerNameAsync(string customerId)
{
Console.WriteLine($"正在读取客户: {customerId}");
await Task.Delay(500);
return "张三";
}
string name = await GetCustomerNameAsync("C001");
Console.WriteLine($"客户名称: {name}");
Task<string> 表示将来会得到一个 string。
例子 6:执行顺序看 await
Console.WriteLine("1. 调用前");
async Task DoWorkAsync()
{
Console.WriteLine("2. 方法开始");
await Task.Delay(500);
Console.WriteLine("3. 方法结束");
}
await DoWorkAsync();
Console.WriteLine("4. 调用后");
输出顺序应该是:
1. 调用前
2. 方法开始
3. 方法结束
4. 调用后await 会等异步方法完成,再继续执行后面的代码。
例子 7:异步异常在 await 处捕获
try
{
async Task<int> LoadCountAsync(int pageSize)
{
await Task.Delay(300);
if (pageSize <= 0)
{
throw new ArgumentException("每页数量必须大于 0");
}
return pageSize * 2;
}
int count = await LoadCountAsync(-1);
Console.WriteLine($"读取数量: {count}");
}
catch (ArgumentException ex)
{
Console.WriteLine($"读取失败: {ex.Message}");
}
异常是在 LoadCountAsync 里面抛出的,但会在 await LoadCountAsync(-1) 这一行被捕获。
例子 8:异步写入和读取文件
string path = "demo.txt";
await File.WriteAllTextAsync(path, "第一行\n第二行\n第三行");
string text = await File.ReadAllTextAsync(path);
Console.WriteLine("文件内容:");
Console.WriteLine(text);文件读写是典型等待场景。能用异步 API 时,优先使用 ReadAllTextAsync、WriteAllTextAsync。
例子 9:异步读取文件并返回列表
string path = "orders.txt";
await File.WriteAllTextAsync(path, "SO-001\nSO-002\nSO-003");
async Task<List<string>> LoadOrdersAsync(string filePath)
{
string text = await File.ReadAllTextAsync(filePath);
return text
.Split('\n', StringSplitOptions.RemoveEmptyEntries)
.ToList();
}
List<string> orders = await LoadOrdersAsync(path);
foreach (string order in orders)
{
Console.WriteLine(order);
}
这个例子是本页最重要的业务形态:异步方法读取数据,然后返回整理好的结果。
例子 10:两个异步任务先后执行
using System.Diagnostics;
Stopwatch stopwatch = Stopwatch.StartNew();
async Task<string> LoadCustomerAsync()
{
await Task.Delay(1000);
return "客户: 张三";
}
async Task<string> LoadOrderAsync()
{
await Task.Delay(1000);
return "订单: SO-001";
}
string customer = await LoadCustomerAsync();
string order = await LoadOrderAsync();
stopwatch.Stop();
Console.WriteLine(customer);
Console.WriteLine(order);
Console.WriteLine($"耗时约: {stopwatch.ElapsedMilliseconds} 毫秒");
这个版本是先等客户,再等订单,总耗时大约 2 秒。
例子 11:两个异步任务同时启动再等待
using System.Diagnostics;
Stopwatch stopwatch = Stopwatch.StartNew();
async Task<string> LoadCustomerAsync()
{
await Task.Delay(1000);
return "客户: 张三";
}
async Task<string> LoadOrderAsync()
{
await Task.Delay(1000);
return "订单: SO-001";
}
Task<string> customerTask = LoadCustomerAsync();
Task<string> orderTask = LoadOrderAsync();
string customer = await customerTask;
string order = await orderTask;
stopwatch.Stop();
Console.WriteLine(customer);
Console.WriteLine(order);
Console.WriteLine($"耗时约: {stopwatch.ElapsedMilliseconds} 毫秒");
这个版本先启动两个任务,再分别等待。总耗时大约 1 秒。
这里先知道现象即可,后面会专门讲并发控制。
例子 12:用 Task.WhenAll 等多个任务
async Task<string> LoadCustomerAsync()
{
await Task.Delay(300);
return "客户: 张三";
}
async Task<string> LoadOrderAsync()
{
await Task.Delay(500);
return "订单: SO-001";
}
async Task<string> LoadProductAsync()
{
await Task.Delay(700);
return "商品: 键盘";
}
Task<string> customerTask = LoadCustomerAsync();
Task<string> orderTask = LoadOrderAsync();
Task<string> productTask = LoadProductAsync();
string[] results = await Task.WhenAll(customerTask, orderTask, productTask);
foreach (string result in results)
{
Console.WriteLine(result);
}
Task.WhenAll 表示等待一组任务全部完成。
常见错误和修法
| 错误 | 为什么错 | 修法 |
|---|---|---|
异步方法忘记 await | 后续代码会提前执行 | 调用返回 Task 的方法时按需要 await |
把 Task.Delay 当线程睡眠 | 它表示异步等待,不堵住当前线程 | 用它模拟等待,别用来替代真实逻辑 |
async void 到处写 | 异常难处理,无法等待 | 除事件处理外优先用 async Task |
方法名不带 Async | 调用方看不出要等待 | 异步方法使用 ReadAsync 这类命名 |
| 在界面线程做耗时工作 | 界面会卡住 | 耗时 IO 使用异步 API |
小白重复敲写训练
异步先练执行顺序,不要一开始就碰网络和数据库。
训练 1:第一次 await
Console.WriteLine("开始");
await Task.Delay(1000);
Console.WriteLine("一秒后结束");第二遍把等待改成 2 秒,观察程序没有立即结束。
训练 2:异步方法返回结果
async Task<int> GetNumberAsync()
{
await Task.Delay(500);
return 42;
}
int result = await GetNumberAsync();
Console.WriteLine(result);
改动任务:让方法接收一个数字并返回它的两倍。
训练 3:比较顺序执行
async Task WorkAsync(string name, int milliseconds)
{
Console.WriteLine($"{name} 开始");
await Task.Delay(milliseconds);
Console.WriteLine($"{name} 结束");
}
await WorkAsync("A", 500);
await WorkAsync("B", 500);
运行前写下四行输出的顺序。
每日小测
做完本页后,用这 5 题检查是否真的掌握。
1. 判断题
本页的目标不是只把代码运行起来,还要能说清楚“为什么这样写”。
答案:对。能运行只是第一步,能解释原理、常用操作和常见错误,才说明本页内容进入了可复用能力。
2. 填空题
本页主题是:异步基础。今天至少要掌握的 3 个点是:
1. 同步阻塞和异步等待有什么区别
2. `Task` 为什么不是结果本身,而是“未来会完成的工作”
3. `async` 和 `await` 分别负责什么答案:以上 3 点必须能用自己的代码跑通,不能只停留在阅读。
3. 流程题
遇到本页相关功能时,先按什么顺序处理?
答案:先看完整例子,确认最终效果;再读原理和名词;然后跟着第三部分从空项目敲代码;最后对照作业答案检查。
4. 找错误题
如果本页代码运行失败,第一步应该做什么?
答案:先看终端或 IDE 里的第一条错误,找到文件名和行号;不要同时改很多地方。再回到本页的“常见错误和修法”表格,对照错误类型逐项排查。
5. 改需求题
在本页完整例子跑通后,至少改一个小需求。
可选改法:
- 改一个字段名称。
- 多加一个校验条件。
- 多输出一行结果。
- 把固定数据改成用户输入。
- 把一次处理改成多条数据处理。
答案标准:修改后能重新运行,并能说明这次修改影响了哪一段逻辑。重点检查:同步阻塞和异步等待有什么区别。
上位机专项练习
设备读取不能卡住界面。先用短小程序看懂 Task、await 和异步返回值。
下面 3 个例子都要亲手敲。先运行原代码,再完成每个例子后面的改动任务。
专项例子 1:等待设备返回温度
class Program
{
static async Task<double> ReadTemperatureAsync()
{
await Task.Delay(500);
return 26.8;
}
static async Task Main()
{
Console.WriteLine("开始读取");
double value = await ReadTemperatureAsync();
Console.WriteLine($"温度: {value:F1} C");
}
}运行结果或界面效果:
开始读取
温度: 26.8 C改动任务: 把等待时间改成 1000 毫秒,观察哪一行先出现。
专项例子 2:同时保留界面响应提示
class Program
{
static async Task<string> ConnectAsync()
{
await Task.Delay(800);
return "PLC 已连接";
}
static async Task Main()
{
Task<string> task = ConnectAsync();
Console.WriteLine("界面仍可显示加载动画");
Console.WriteLine(await task);
}
}运行结果或界面效果:
界面仍可显示加载动画
PLC 已连接改动任务: 把返回文字改成“温控器已连接”。
专项例子 3:异步返回设备状态
class Program
{
static async Task<bool> IsOnlineAsync()
{
await Task.Delay(300);
return true;
}
static async Task Main()
{
bool online = await IsOnlineAsync();
Console.WriteLine(online ? "在线" : "离线");
}
}运行结果或界面效果:
在线改动任务: 把返回值改成 false,确认显示离线。
第四部分:作业完整答案
这一部分给出当天作业的完整答案。建议先照着敲一遍,再修改文件内容和等待时间验证。
作业 1:异步返回订单数量
要求:
- 写一个
LoadOrderCountAsync方法。 - 方法内部等待 500 毫秒。
- 返回订单数量。
- 调用处用
await拿到结果。
完整答案
async Task<int> LoadOrderCountAsync()
{
Console.WriteLine("开始读取订单数量");
await Task.Delay(500);
return 3;
}
int count = await LoadOrderCountAsync();
Console.WriteLine($"读取到 {count} 条订单");
作业 2:异步读取文件
要求:
- 先异步写入一个文件。
- 再异步读取这个文件。
- 输出文件内容。
完整答案
string path = "orders.txt";
string content = "SO-001\nSO-002\nSO-003";
await File.WriteAllTextAsync(path, content);
string text = await File.ReadAllTextAsync(path);
Console.WriteLine("读取结果:");
Console.WriteLine(text);作业 3:异步读取订单列表
要求:
- 文件里每一行是一条订单号。
- 写
LoadOrdersAsync返回List<string>。 - 读取后输出订单数量和订单号。
完整答案
string path = "orders.txt";
await File.WriteAllTextAsync(path, "SO-001\nSO-002\nSO-003");
async Task<List<string>> LoadOrdersAsync(string filePath)
{
if (!File.Exists(filePath))
{
return new List<string>();
}
string text = await File.ReadAllTextAsync(filePath);
return text
.Split('\n', StringSplitOptions.RemoveEmptyEntries)
.ToList();
}
List<string> orders = await LoadOrdersAsync(path);
Console.WriteLine($"订单数量: {orders.Count}");
foreach (string order in orders)
{
Console.WriteLine(order);
}
作业 4:异步异常处理
要求:
- 写
LoadCustomerAsync。 - 如果客户编号为空,抛出异常。
- 调用处用
try/catch捕获。
完整答案
try
{
async Task<string> LoadCustomerAsync(string customerId)
{
await Task.Delay(300);
if (string.IsNullOrWhiteSpace(customerId))
{
throw new ArgumentException("客户编号不能为空");
}
return "张三";
}
string customerName = await LoadCustomerAsync("");
Console.WriteLine(customerName);
}
catch (ArgumentException ex)
{
Console.WriteLine($"读取客户失败: {ex.Message}");
}
作业 5:同时读取三类数据
要求:
- 写三个异步方法:读取客户、订单、商品。
- 同时启动三个任务。
- 用
Task.WhenAll等它们全部完成。 - 输出结果。
完整答案
async Task<string> LoadCustomerAsync()
{
await Task.Delay(300);
return "客户: 张三";
}
async Task<string> LoadOrderAsync()
{
await Task.Delay(500);
return "订单: SO-001";
}
async Task<string> LoadProductAsync()
{
await Task.Delay(700);
return "商品: 键盘";
}
Task<string> customerTask = LoadCustomerAsync();
Task<string> orderTask = LoadOrderAsync();
Task<string> productTask = LoadProductAsync();
string[] results = await Task.WhenAll(customerTask, orderTask, productTask);
foreach (string result in results)
{
Console.WriteLine(result);
}
本页最后要记住
Task表示未来会完成的工作,不是结果本身。Task<T>表示未来会得到一个T类型结果。async允许方法内部使用await。await等任务完成,并拿到结果或继续后续代码。- 异步主要解决等待问题,不是让所有计算自动变快。
- 能
await就不要用.Result或.Wait()。 - 异步异常可以在
await那一行用try/catch捕获。