Week 04 - 阶段检查
这一页用于确认第四周内容是否已经能实际使用。每个检查点都给完整代码,至少要跑通一遍。
本周学习地图
| 阶段 | 先掌握什么 | 最终能写什么 |
|---|---|---|
| 异步基础 | Task、Task<T>、await | 能等待异步结果 |
| 异步 IO | 文件读取、文件写入 | 能不阻塞地处理文件 |
| 多任务 | Task.WhenAll、Task.WhenAny | 能同时处理多个任务 |
| 限流 | SemaphoreSlim | 能控制并发数量 |
| 取消 | CancellationToken | 能支持取消和超时 |
| 失败处理 | 异步异常、结果对象 | 能统计成功和失败 |
本周自测项目:异步文件批处理器
从空项目开始写一个批处理器,至少包含:
- 创建 3 个正常文件和 1 个缺失路径。
- 异步读取所有文件。
- 同时最多处理 2 个文件。
- 支持 5 秒超时。
- 每个文件返回成功状态、长度、耗时、错误原因。
- 最后输出成功数和失败数。
过关标准:能解释为什么 Release() 必须放进 finally,为什么批处理要返回结果对象。
上位机阶段验收:可取消的多设备异步轮询器
本周目标: 并发读取多台设备,同时支持限流、超时、取消和失败汇总。
必须亲手完成:
- Task.WhenAll 并发读取至少 3 台设备
- SemaphoreSlim 限制同时连接数
- 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
你要会什么:
- 写一个返回
Task<int>的方法。 - 调用处用
await得到真正的int。 - 能解释
Task<int>不是int。
完整答案
int count = await LoadCountAsync();
Console.WriteLine($"读取数量: {count}");
async Task<int> LoadCountAsync()
{
Console.WriteLine("开始读取数量");
await Task.Delay(300);
return 3;
}验收标准:
- 能输出数量。
- 能解释
await之后才拿到int。 - 能把返回值改成
Task<string>并跑通。
检查 2:异步 IO
你要会什么:
- 异步写文件。
- 异步读文件。
- 把文件内容拆成行。
完整答案
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);
}验收标准:
- 能写入文件。
- 能读取文件。
- 能输出 3 行订单号。
检查 3:Task.WhenAll
你要会什么:
- 同时启动多个异步任务。
- 等全部任务完成。
- 收集所有结果。
完整答案
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}";
}验收标准:
- 能输出 3 个订单。
- 能解释
Task.WhenAll是等全部完成。 - 能把任务数量改成 5 个。
检查 4:Task.WhenAny
你要会什么:
- 同时启动两个任务。
- 等最快完成的那个。
- 再
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 "接口结果";
}验收标准:
- 能输出缓存结果。
- 能解释
WhenAny不会自动取消慢任务。 - 能把等待时间对调后观察结果变化。
检查 5:限制并发
你要会什么:
- 使用
SemaphoreSlim。 WaitAsync拿名额。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();
}
}验收标准:
- 一次最多 2 个任务进入处理区。
- 把并发上限改成
1后更接近顺序执行。 - 能解释为什么
Release必须在finally。
检查 6:取消和超时
你要会什么:
- 创建超时取消源。
- 把 token 传给真正等待的地方。
- 单独捕获取消异常。
完整答案
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);
}
}验收标准:
- 任务会在完成前被取消。
- 程序不会崩溃。
- 能解释取消是协作式的。
检查 7:异步异常处理
你要会什么:
- 异步方法内部抛异常。
- 调用处在
await位置捕获。 - 取消异常和普通异常分开。
完整答案
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 "张三";
}验收标准:
- 参数错误能被捕获。
- 能解释异常在
await LoadCustomerAsync("")处进入catch。 - 能把空字符串改成
"C001"并跑通正常流程。
检查 8:线程和 Task 的区别
你要会什么:
- 能看到当前进程编号。
- 能看到当前线程编号。
- 能看到
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}");验收标准:
- 能输出进程 Id。
- 能输出线程 Id。
- 能解释
Task.Run表示把工作交给线程池,不等于所有异步都需要Task.Run。
检查 9:什么时候用 Task.Run
你要会什么:
- IO 等待优先用异步 IO。
- CPU 计算可以考虑
Task.Run。 - 不要用
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;
}验收标准:
- 能输出质数数量。
- 能解释这是 CPU 计算,不是文件或网络等待。
- 能解释文件读取应该优先用
File.ReadAllTextAsync,不是Task.Run(() => File.ReadAllText(...))。
第三部分:综合作业完整答案
要求:
- 创建两个存在文件和一个不存在文件。
- 并发读取所有文件。
- 一次最多处理 2 个。
- 支持取消。
- 每项返回结果对象。
- 输出成功数、失败数和每项结果。
完整答案
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后仍能运行。 - 超时时间改得很短时,取消分支能出现。
Release放在finally。- 所有异步调用都用
await,没有.Result或.Wait()。
第四部分:进入下一周前的最低通过线
进入下一周前,至少做到:
- 能解释
Task、线程、线程池不是一回事。 - 能写
async Task<T>并用await拿到结果。 - 能用异步 API 读取文件。
- 能用
Task.WhenAll等多个任务。 - 能用
SemaphoreSlim限制并发。 - 能用
CancellationToken取消任务。 - 能在
await处捕获异步异常。 - 能写一个结果对象统计批量任务成功失败。
如果这 8 条能做到,第四周就不是只会写 await,而是已经能写可控的异步流程。