Skip to Content
Week 04Day 1 - 异步基础

Day 1 - 异步基础

建议用时:240-300 分钟

你将学会什么

  • 同步阻塞和异步等待有什么区别
  • Task 为什么不是结果本身,而是“未来会完成的工作”
  • asyncawait 分别负责什么
  • TaskTask<T>void 在异步方法里怎么选择
  • 异步异常应该怎么捕获
  • 文件读取这类真实等待场景怎么写异步代码

异步不是让所有代码都变快。异步主要解决“等待期间不要把线程卡死”的问题,尤其是文件、网络、数据库、接口请求这些需要等待外部结果的场景。

本页固定顺序

  1. 先学第一部分:弄懂今天最小、最重要的知识,并运行短例子。
  2. 再学第二部分:把刚学的知识组合成一个完整例子。
  3. 然后做第三部分:自己跟着敲,再完成重复训练和每日小测。
  4. 最后做第四部分:先独立完成作业,再用完整答案检查。

学习衔接

上一页学习的是“第三周复盘与综合练习”,今天继续学习“异步基础”。先使用上一页已经会的写法,再只增加今天这个新知识点;如果前置内容还不能独立敲出,先回上一页复习,不要硬跳。

今天的最低通过线

第一次学习不要求背完整页。完成下面 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

如果方法返回 Taskawait 之后没有返回值,只表示等它完成。

6. TaskTask<T> 怎么选

方法要做什么返回类型
异步保存文件,不需要返回结果Task
异步读取数量,需要返回 intTask<int>
异步读取文本,需要返回 stringTask<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> 调用处使用 await

9. 为什么不要随便用 .Result.Wait()

await 是异步等待。

.Result.Wait() 是同步阻塞。

如果在异步代码里乱用 .Result.Wait(),可能导致:

  1. 当前线程被堵住。
  2. 程序响应变慢。
  3. 某些环境下出现死锁。
  4. 异常信息更难读。

所以本课程先定一个规则:

能 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. 打开一个控制台项目。
  2. 每次只保留一个例子的代码,运行通过后再换下一个。
  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 时,优先使用 ReadAllTextAsyncWriteAllTextAsync

例子 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:异步返回订单数量

要求:

  1. 写一个 LoadOrderCountAsync 方法。
  2. 方法内部等待 500 毫秒。
  3. 返回订单数量。
  4. 调用处用 await 拿到结果。

完整答案

async Task<int> LoadOrderCountAsync() { Console.WriteLine("开始读取订单数量"); await Task.Delay(500); return 3; } int count = await LoadOrderCountAsync(); Console.WriteLine($"读取到 {count} 条订单");

作业 2:异步读取文件

要求:

  1. 先异步写入一个文件。
  2. 再异步读取这个文件。
  3. 输出文件内容。

完整答案

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:异步读取订单列表

要求:

  1. 文件里每一行是一条订单号。
  2. LoadOrdersAsync 返回 List<string>
  3. 读取后输出订单数量和订单号。

完整答案

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:异步异常处理

要求:

  1. LoadCustomerAsync
  2. 如果客户编号为空,抛出异常。
  3. 调用处用 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:同时读取三类数据

要求:

  1. 写三个异步方法:读取客户、订单、商品。
  2. 同时启动三个任务。
  3. Task.WhenAll 等它们全部完成。
  4. 输出结果。

完整答案

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); }

本页最后要记住

  1. Task 表示未来会完成的工作,不是结果本身。
  2. Task<T> 表示未来会得到一个 T 类型结果。
  3. async 允许方法内部使用 await
  4. await 等任务完成,并拿到结果或继续后续代码。
  5. 异步主要解决等待问题,不是让所有计算自动变快。
  6. await 就不要用 .Result.Wait()
  7. 异步异常可以在 await 那一行用 try/catch 捕获。