外观
异步编程进阶
🧱 场景假设:你要“读一个大文件”,然后“处理每一行数据”
比如:文件有 100 万行,每行是个日志。你要读出来 → 分析 → 存数据库。
✅ 第1点:别一边读一边处理!要“生产线”干活
❌ 低效写法(顺序):
var lines = File.ReadAllLines("big.log");
foreach (var line in lines)
{
Analyze(line); // 假设很慢,比如要做计算或查数据库
}问题:读文件时 CPU 闲着;分析时磁盘闲着。像一个人既当司机又当厨师,效率低。
✅ 进阶写法(两人配合):
- A 专门读文件 → 把数据扔进一个“传送带”(叫 Channel)
- B 专门处理数据 ← 从传送带上拿数据
代码简化版(假装 Channel 是个 List + 锁):
var queue = new Queue<string>(); // 传送带(实际用 Channel 更安全高效)
bool finished = false;
// A:读文件(生产者)
Task.Run(() =>
{
foreach (var line in File.ReadLines("big.log"))
{
lock (queue) queue.Enqueue(line);
}
finished = true;
});
// B:处理数据(消费者)
while (!finished || queue.Count > 0)
{
string line = null;
lock (queue)
if (queue.Count > 0) line = queue.Dequeue();
if (line != null)
Analyze(line); // 真正处理
}💡 核心逻辑:I/O(读文件)和 CPU(分析)分开干,互不等对方。
✅ 第2点:别每次循环都 await!能“马上拿到结果”就直接拿
❌ 低效写法(每次都 await):
for (int i = 0; i < 1000; i++)
{
var result = await GetDataAsync(i); // 每次都等
Console.WriteLine(result);
}问题:如果 GetDataAsync 90% 的时候 其实已经完成了(比如缓存命中),你还非要走“排队 → 等待 → 回调”流程,浪费时间。
✅ 进阶写法(先看一眼“好了没”):
for (int i = 0; i < 1000; i++)
{
var task = GetDataAsync(i);
if (task.IsCompleted) // 如果已经好了,直接拿!
{
Console.WriteLine(task.Result);
}
else // 真的需要等,才 await
{
Console.WriteLine(await task);
}
}💡 核心逻辑:能“立刻拿到”就别排队,省下调度开销。
✅ 第3点:异常别乱抛!能“悄悄处理”就别炸
❌ 低效写法(直接 await,自动抛异常):
try
{
await DoSomethingAsync();
}
catch (Exception ex)
{
Console.WriteLine("出错了:" + ex.Message);
}问题:每次异常都要构造堆栈、分配内存、展开调用栈——很贵!
✅ 进阶写法(.NET 8 起可用):
var task = DoSomethingAsync();
await task.ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing); // 不自动抛异常!
if (task.IsFaulted)
{
Console.WriteLine("出错了(没抛):" + task.Exception.Message);
}
else
{
var result = task.Result;
}💡 核心逻辑:异常是“奢侈品”,不是“日常用品”。能悄悄检查就别让它炸出来。
✅ 第4点:别套娃 Task!一层就够了
❌ 低效写法(返回 Task<Task>):
var outerTask = Task.Run(async () =>
{
await DoSomethingAsync(); // 返回 Task
});
// outerTask 类型其实是 Task<Task>这就像:你点外卖,结果送来一个快递盒,里面还是个快递盒……
✅ 进阶写法(用 Unwrap 拆开):
var innerTask = Task.Run(() => DoSomethingAsync()); // 返回 Task<Task<T>>
var realTask = innerTask.Unwrap(); // 拆成 Task<T>
await realTask;💡 核心逻辑:别让 Task 包 Task,用 Unwrap 拆干净。
✅ 第5点:后台任务别回主线程!
❌ 低效写法(在 ASP.NET 或 WPF 里):
await Task.Run(() => HeavyWork()); // 看似后台,其实可能偷偷回主线程在 Web 或 UI 程序里,await 默认会“回到原来的线程”(比如页面线程),可能卡住界面!
✅ 进阶写法(明确说:我要纯后台!):
await Task.Factory.StartNew(
() => HeavyWork(),
CancellationToken.None,
TaskCreationOptions.DenyChildAttach,
TaskScheduler.Default // 明确:用线程池,别回主线程!
);💡 核心逻辑:后台任务就彻底后台,别和主线程扯上关系。
🎯 总结:
异步不是“加个 await 就完事”,而是“像安排工人一样安排任务”:
- 谁干 I/O?谁干 CPU?
- 能马上干完就别排队;
- 出错了别乱吼,悄悄记下来;
- 别套娃;
- 后台工人别回办公室!
