Skip to Content
Week 04📋 Week 04 阶段检查

Week 04 - 阶段检查

这一页用于确认第四周内容是否已经能实际使用。每个检查点都给完整代码,至少要跑通一遍。

本周学习地图

阶段先掌握什么最终能写什么
异步基础TaskTask<T>await能等待异步结果
异步 IO文件读取、文件写入能不阻塞地处理文件
多任务Task.WhenAllTask.WhenAny能同时处理多个任务
限流SemaphoreSlim能控制并发数量
取消CancellationToken能支持取消和超时
失败处理异步异常、结果对象能统计成功和失败

本周自测项目:异步文件批处理器

从空项目开始写一个批处理器,至少包含:

  • 创建 3 个正常文件和 1 个缺失路径。
  • 异步读取所有文件。
  • 同时最多处理 2 个文件。
  • 支持 5 秒超时。
  • 每个文件返回成功状态、长度、耗时、错误原因。
  • 最后输出成功数和失败数。

过关标准:能解释为什么 Release() 必须放进 finally,为什么批处理要返回结果对象。

上位机阶段验收:可取消的多设备异步轮询器

本周目标: 并发读取多台设备,同时支持限流、超时、取消和失败汇总。

必须亲手完成:

  1. Task.WhenAll 并发读取至少 3 台设备
  2. SemaphoreSlim 限制同时连接数
  3. CancellationToken 停止轮询并统计成功失败

过关标准: 程序不会因单台设备失败而退出,你能解释 await、并发、取消和 finally。

不要只看答案。新建一个空项目重做一次,运行成功后再故意改坏一处并自己排错。

本周流程图

准备一组文件路径 -> 每个路径创建一个 Task -> SemaphoreSlim 控制同时数量 -> File.ReadAllTextAsync 异步读取 -> CancellationToken 支持取消和超时 -> try/catch 把失败转成结果对象 -> Task.WhenAll 等全部完成 -> 汇总成功、失败、耗时和错误原因

第一部分:本周原理地图

第四周主线是:让程序在等待文件、网络、数据库或批量任务时,仍然保持可控。

知识点解决的问题典型写法
Task表示未来会完成的工作Task<int>
await等异步任务完成并拿结果await LoadAsync()
异步 IO等文件、网络、数据库时不硬堵File.ReadAllTextAsync
Task.WhenAll等多个任务全部完成await Task.WhenAll(tasks)
Task.WhenAny等最快完成的任务await Task.WhenAny(tasks)
SemaphoreSlim限制同一时间运行数量new SemaphoreSlim(3)
CancellationToken取消或超时Task.Delay(1000, token)
异步异常await 处捕获失败try { await ... } catch { ... }
结果对象批量任务统计成功失败FileResult

进程、线程、线程池、Task、async/await

名词直接解释
进程一个正在运行的程序
线程进程里执行代码的路线
线程池.NET 管理的一组可复用后台线程
Task一件未来会完成的工作
async/await把等待点拆开,完成后继续执行
协程式异步遇到等待时暂停当前方法,之后再恢复执行

必须分清:

Task 不等于 Thread。 async/await 不等于一定新开线程。 异步 IO 不等于 CPU 计算变快。

第二部分:阶段检查题

下面每道题都给完整答案。你要确认自己能解释为什么这样写。

检查 1:Task<T>await

你要会什么:

  1. 写一个返回 Task<int> 的方法。
  2. 调用处用 await 得到真正的 int
  3. 能解释 Task<int> 不是 int

完整答案

int count = await LoadCountAsync(); Console.WriteLine($"读取数量: {count}"); async Task<int> LoadCountAsync() { Console.WriteLine("开始读取数量"); await Task.Delay(300); return 3; }

验收标准:

  1. 能输出数量。
  2. 能解释 await 之后才拿到 int
  3. 能把返回值改成 Task<string> 并跑通。

检查 2:异步 IO

你要会什么:

  1. 异步写文件。
  2. 异步读文件。
  3. 把文件内容拆成行。

完整答案

string path = "orders.txt"; await File.WriteAllTextAsync(path, "SO-001\nSO-002\nSO-003"); string text = await File.ReadAllTextAsync(path); string[] lines = text.Split('\n', StringSplitOptions.RemoveEmptyEntries); Console.WriteLine($"订单数量: {lines.Length}"); foreach (string line in lines) { Console.WriteLine(line); }

验收标准:

  1. 能写入文件。
  2. 能读取文件。
  3. 能输出 3 行订单号。

检查 3:Task.WhenAll

你要会什么:

  1. 同时启动多个异步任务。
  2. 等全部任务完成。
  3. 收集所有结果。

完整答案

int[] ids = { 1, 2, 3 }; Task<string>[] tasks = ids .Select(id => LoadOrderAsync(id)) .ToArray(); string[] orders = await Task.WhenAll(tasks); foreach (string order in orders) { Console.WriteLine(order); } async Task<string> LoadOrderAsync(int id) { await Task.Delay(300); return $"订单 SO-{id:000}"; }

验收标准:

  1. 能输出 3 个订单。
  2. 能解释 Task.WhenAll 是等全部完成。
  3. 能把任务数量改成 5 个。

检查 4:Task.WhenAny

你要会什么:

  1. 同时启动两个任务。
  2. 等最快完成的那个。
  3. await 这个最快任务拿结果。

完整答案

Task<string> cacheTask = LoadCacheAsync(); Task<string> apiTask = LoadApiAsync(); Task<string> firstTask = await Task.WhenAny(cacheTask, apiTask); string result = await firstTask; Console.WriteLine($"最快结果: {result}"); async Task<string> LoadCacheAsync() { await Task.Delay(300); return "缓存结果"; } async Task<string> LoadApiAsync() { await Task.Delay(1000); return "接口结果"; }

验收标准:

  1. 能输出缓存结果。
  2. 能解释 WhenAny 不会自动取消慢任务。
  3. 能把等待时间对调后观察结果变化。

检查 5:限制并发

你要会什么:

  1. 使用 SemaphoreSlim
  2. WaitAsync 拿名额。
  3. Release 放在 finally

完整答案

int[] ids = Enumerable.Range(1, 6).ToArray(); using SemaphoreSlim semaphore = new SemaphoreSlim(2); Task[] tasks = ids .Select(id => ProcessAsync(id, semaphore)) .ToArray(); await Task.WhenAll(tasks); async Task ProcessAsync(int id, SemaphoreSlim gate) { await gate.WaitAsync(); try { Console.WriteLine($"开始任务 {id}"); await Task.Delay(500); Console.WriteLine($"完成任务 {id}"); } finally { gate.Release(); } }

验收标准:

  1. 一次最多 2 个任务进入处理区。
  2. 把并发上限改成 1 后更接近顺序执行。
  3. 能解释为什么 Release 必须在 finally

检查 6:取消和超时

你要会什么:

  1. 创建超时取消源。
  2. 把 token 传给真正等待的地方。
  3. 单独捕获取消异常。

完整答案

using CancellationTokenSource cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(800)); try { await WorkAsync(cts.Token); } catch (OperationCanceledException) { Console.WriteLine("任务已取消或超时"); } async Task WorkAsync(CancellationToken token) { for (int i = 1; i <= 10; i++) { Console.WriteLine($"处理第 {i} 步"); await Task.Delay(300, token); } }

验收标准:

  1. 任务会在完成前被取消。
  2. 程序不会崩溃。
  3. 能解释取消是协作式的。

检查 7:异步异常处理

你要会什么:

  1. 异步方法内部抛异常。
  2. 调用处在 await 位置捕获。
  3. 取消异常和普通异常分开。

完整答案

try { string customer = await LoadCustomerAsync(""); Console.WriteLine(customer); } catch (OperationCanceledException) { Console.WriteLine("读取已取消"); } catch (ArgumentException ex) { Console.WriteLine($"参数错误: {ex.Message}"); } catch (Exception ex) { Console.WriteLine($"读取失败: {ex.Message}"); } async Task<string> LoadCustomerAsync(string customerId) { await Task.Delay(300); if (string.IsNullOrWhiteSpace(customerId)) { throw new ArgumentException("客户编号不能为空"); } return "张三"; }

验收标准:

  1. 参数错误能被捕获。
  2. 能解释异常在 await LoadCustomerAsync("") 处进入 catch
  3. 能把空字符串改成 "C001" 并跑通正常流程。

检查 8:线程和 Task 的区别

你要会什么:

  1. 能看到当前进程编号。
  2. 能看到当前线程编号。
  3. 能看到 Task.Run 可能在线程池线程上执行。

完整答案

using System.Threading; Console.WriteLine($"进程 Id: {Environment.ProcessId}"); Console.WriteLine($"主线程 Id: {Thread.CurrentThread.ManagedThreadId}"); await Task.Run(() => { Console.WriteLine($"Task.Run 内部线程 Id: {Thread.CurrentThread.ManagedThreadId}"); }); Console.WriteLine($"await 返回后线程 Id: {Thread.CurrentThread.ManagedThreadId}");

验收标准:

  1. 能输出进程 Id。
  2. 能输出线程 Id。
  3. 能解释 Task.Run 表示把工作交给线程池,不等于所有异步都需要 Task.Run

检查 9:什么时候用 Task.Run

你要会什么:

  1. IO 等待优先用异步 IO。
  2. CPU 计算可以考虑 Task.Run
  3. 不要用 Task.Run 包住所有异步方法。

完整答案

int primeCount = await Task.Run(() => CountPrimes(20000)); Console.WriteLine($"质数数量: {primeCount}"); int CountPrimes(int max) { int count = 0; for (int number = 2; number <= max; number++) { if (IsPrime(number)) { count++; } } return count; } bool IsPrime(int number) { for (int divisor = 2; divisor * divisor <= number; divisor++) { if (number % divisor == 0) { return false; } } return true; }

验收标准:

  1. 能输出质数数量。
  2. 能解释这是 CPU 计算,不是文件或网络等待。
  3. 能解释文件读取应该优先用 File.ReadAllTextAsync,不是 Task.Run(() => File.ReadAllText(...))

第三部分:综合作业完整答案

要求:

  1. 创建两个存在文件和一个不存在文件。
  2. 并发读取所有文件。
  3. 一次最多处理 2 个。
  4. 支持取消。
  5. 每项返回结果对象。
  6. 输出成功数、失败数和每项结果。

完整答案

await File.WriteAllTextAsync("a.txt", "AAA"); await File.WriteAllTextAsync("b.txt", "BBBB"); string[] paths = { "a.txt", "missing.txt", "b.txt" }; using CancellationTokenSource cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); using SemaphoreSlim semaphore = new SemaphoreSlim(2); Task<FileResult>[] tasks = paths .Select(path => ReadFileWithLimitAsync(path, semaphore, cts.Token)) .ToArray(); FileResult[] results = await Task.WhenAll(tasks); Console.WriteLine($"成功: {results.Count(result => result.Success)}"); Console.WriteLine($"失败: {results.Count(result => !result.Success)}"); foreach (FileResult result in results) { Console.WriteLine($"{result.Path} | {result.Success} | {result.Message}"); } async Task<FileResult> ReadFileWithLimitAsync(string path, SemaphoreSlim gate, CancellationToken token) { await gate.WaitAsync(token); try { return await ReadFileSafeAsync(path, token); } finally { gate.Release(); } } async Task<FileResult> ReadFileSafeAsync(string path, CancellationToken token) { try { string text = await File.ReadAllTextAsync(path, token); return new FileResult(path, true, $"长度 {text.Length}"); } catch (OperationCanceledException) { return new FileResult(path, false, "已取消"); } catch (Exception ex) { return new FileResult(path, false, ex.Message); } } class FileResult { public FileResult(string path, bool success, string message) { Path = path; Success = success; Message = message; } public string Path { get; } public bool Success { get; } public string Message { get; } }

综合验收标准

完成后必须逐条检查:

  1. 正常文件能读取成功。
  2. 不存在文件会返回失败结果。
  3. 一个文件失败不会影响其他文件。
  4. 并发上限改成 1 后仍能运行。
  5. 超时时间改得很短时,取消分支能出现。
  6. Release 放在 finally
  7. 所有异步调用都用 await,没有 .Result.Wait()

第四部分:进入下一周前的最低通过线

进入下一周前,至少做到:

  1. 能解释 Task、线程、线程池不是一回事。
  2. 能写 async Task<T> 并用 await 拿到结果。
  3. 能用异步 API 读取文件。
  4. 能用 Task.WhenAll 等多个任务。
  5. 能用 SemaphoreSlim 限制并发。
  6. 能用 CancellationToken 取消任务。
  7. 能在 await 处捕获异步异常。
  8. 能写一个结果对象统计批量任务成功失败。

如果这 8 条能做到,第四周就不是只会写 await,而是已经能写可控的异步流程。