同步 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 完成时的回调"。线程不阻塞,运行时把"回调"接回继续执行。

写代码时像写同步——记住三件事:

  1. 调用异步 API 时 await
  2. 调用方法签名加 async Task / async Task<T>
  3. 别用 .Result / .Wait()

→ 下一篇 模式匹配