同步 vs 异步
// 同步:线程被网络等待阻塞
public string GetData()
{
var client = new HttpClient();
return client.GetStringAsync("https://api/data").Result;
}
// 异步:等待时让出线程
public async Task<string> GetDataAsync()
{
var client = new HttpClient();
return await client.GetStringAsync("https://api/data");
}
异步不是多线程——是"等 I/O 时把线程释放给别人用"。在 Web 服务里这意味着同样的线程数能处理多得多的请求。
基本语法
public async Task<int> AddAsync(int a, int b)
{
await Task.Delay(100); // 模拟异步
return a + b;
}
// 调用
int result = await AddAsync(1, 2);
3 个关键字:
async:标记方法是异步的(让你能 await)await:等待 Task 完成,拿到结果Task/Task<T>:异步操作的返回类型
返回类型
async Task DoSomething() { await ...; } // 无返回值
async Task<int> Compute() { await ...; return 42; } // 有返回值
async ValueTask<int> Fast() { ... } // 高频 / 多数同步完成(性能优化)
async void OnClick() { ... } // 事件处理器(仅此一处用 void)
永远不要写 async void——除非是事件处理。async void 方法抛错无法被外层 try/catch 捕获,会让整个进程崩溃。
并发:多个 Task 一起跑
// ❌ 串行:总耗时 = a + b + c
var a = await FetchAsync("/a");
var b = await FetchAsync("/b");
var c = await FetchAsync("/c");
// ✅ 并行:总耗时 = max(a, b, c)
var ta = FetchAsync("/a");
var tb = FetchAsync("/b");
var tc = FetchAsync("/c");
var (a, b, c) = (await ta, await tb, await tc);
// 或
var results = await Task.WhenAll(ta, tb, tc);
Task.WhenAll:等所有完成。Task.WhenAny:任一完成。
取消(CancellationToken)
public async Task<string> FetchAsync(string url, CancellationToken ct)
{
using var client = new HttpClient();
return await client.GetStringAsync(url, ct);
}
// 调用方控制
var cts = new CancellationTokenSource();
cts.CancelAfter(TimeSpan.FromSeconds(5)); // 5 秒后取消
try
{
var data = await FetchAsync("...", cts.Token);
}
catch (OperationCanceledException)
{
Console.WriteLine("超时");
}
任何长跑 API 都应该接受 CancellationToken。
异常
try
{
await FetchAsync("...");
}
catch (HttpRequestException ex)
{
Console.WriteLine($"网络错误: {ex.Message}");
}
await 让 Task 抛出的异常像同步代码一样被 catch。
Task.WhenAll 失败时:
try
{
await Task.WhenAll(t1, t2, t3);
}
catch
{
// 只能拿到一个异常
}
// 拿到所有异常:检查 task.Exception
ConfigureAwait
public async Task<string> FetchAsync()
{
var data = await client.GetStringAsync(...).ConfigureAwait(false);
return data;
}
ConfigureAwait(false) 意思:"await 后不需要回到原同步上下文"。
- 库代码:永远
.ConfigureAwait(false)——库不知道调用方在 WinForms / WPF / ASP.NET 的什么上下文 - 应用代码(ASP.NET Core):可以省略——ASP.NET Core 没有同步上下文,等价于
false - 桌面应用(WPF / WinForms):UI 代码要回 UI 线程,省略 ConfigureAwait
新 ASP.NET Core 项目可以全部省略——是历史包袱用户的问题。
死锁陷阱
// ❌ 死锁经典 case(WPF / 老 ASP.NET)
public string Get()
{
return GetAsync().Result; // 阻塞线程
}
private async Task<string> GetAsync()
{
return await client.GetStringAsync("..."); // 想回原线程,但原线程被 .Result 阻塞 → 死锁
}
铁律:异步代码全程异步。不要在中间用 .Result / .Wait() / GetAwaiter().GetResult()。
如果调用方真的要同步入口(如 Main 在 .NET 6 之前):让最顶层用 await,下面全部异步。
async 在每一层
// 错觉:"反正最后会 await"
public Task<int> Compute() => InnerAsync(); // 转发 Task
// 这样就不需要 await——但调用方拿到的 Task 已经 in-flight,错误堆栈中省一层
一般写法:
public async Task<int> Compute()
{
return await InnerAsync();
}
性能不敏感场景写 async / await——错误堆栈更清晰,调试容易。
高频 + 多数同步 → ValueTask
public async ValueTask<int> GetFromCache(string key)
{
if (_cache.TryGetValue(key, out var v)) return v;
return await LoadAsync(key);
}
ValueTask<T> 在"同步路径"上不分配 Task 对象——避免 GC 压力。只在剖析说慢时优化,普通方法用 Task 就行。
async 流(IAsyncEnumerable)
public async IAsyncEnumerable<string> ReadLinesAsync(string path)
{
using var reader = File.OpenText(path);
string? line;
while ((line = await reader.ReadLineAsync()) is not null)
{
yield return line;
}
}
// 用
await foreach (var line in ReadLinesAsync("data.txt"))
{
Console.WriteLine(line);
}
C# 8+。流式读大文件 / 流式 API 客户端的标准模式。
心智模型
把 await 想成"暂停这个方法、把后面的代码注册为 Task 完成时的回调"。线程不阻塞,运行时把"回调"接回继续执行。
写代码时像写同步——记住三件事:
- 调用异步 API 时
await - 调用方法签名加
async Task/async Task<T> - 别用
.Result/.Wait()
→ 下一篇 模式匹配