async/await 异步编程深入
大约 11 分钟约 3413 字
async/await 异步编程深入
简介
async/await 是 C# 5.0 引入的异步编程模型,让异步代码像同步代码一样直观。它基于 Task 和 Task<T> 类型,编译器自动生成状态机,避免回调地狱。掌握 async/await 是现代 .NET 开发的基础,涉及 I/O 操作、网络请求、数据库查询等几乎所有异步场景。
特点
async/await 基础
异步方法定义
/// <summary>
/// async/await 基本用法
/// </summary>
// 异步方法返回 Task(无返回值)
public async Task DoSomethingAsync()
{
await Task.Delay(1000); // 模拟耗时操作
Console.WriteLine("操作完成");
}
// 异步方法返回 Task<T>(有返回值)
public async Task<string> GetDataAsync()
{
using var client = new HttpClient();
var response = await client.GetStringAsync("https://api.example.com/data");
return response;
}
// 异步方法返回 ValueTask<T>(避免不必要的 Task 分配)
public async ValueTask<int> GetCountAsync()
{
if (_cache.TryGetValue("count", out int cached))
return cached; // 同步返回,零分配
var count = await _repository.GetCountAsync();
_cache["count"] = count;
return count;
}
// 调用异步方法
public async Task ProcessAsync()
{
try
{
var data = await GetDataAsync();
Console.WriteLine($"获取到数据:{data.Length} 字符");
}
catch (HttpRequestException ex)
{
Console.WriteLine($"请求失败:{ex.Message}");
}
}async void vs async Task
/// <summary>
/// async void 和 async Task 的区别
/// </summary>
// async void — 仅用于事件处理器,异常会导致进程崩溃
private async void Button_Click(object sender, EventArgs e)
{
await DoWorkAsync(); // 事件处理器可以用 async void
}
// async Task — 推荐的异步返回类型
public async Task DoWorkAsync()
{
await Task.Delay(100);
}
// 不推荐 async void(非事件处理器场景)
public async void BadExample()
{
await Task.Delay(100);
throw new Exception("调用方无法捕获此异常!");
}
// 正确做法
public async Task GoodExample()
{
await Task.Delay(100);
throw new Exception("调用方可以 await 并捕获");
}并发控制
Task.WhenAll 并行执行
/// <summary>
/// 并行执行多个异步任务
/// </summary>
public async Task<List<User>> GetUsersParallelAsync(List<int> userIds)
{
// 并行发起所有请求
var tasks = userIds.Select(id => GetUserAsync(id)).ToList();
// 等待所有完成
var users = await Task.WhenAll(tasks);
return users.ToList();
}
// 带超时的并行执行
public async Task<List<User>> GetUsersWithTimeoutAsync(List<int> userIds)
{
var tasks = userIds.Select(id => GetUserAsync(id)).ToList();
var allTask = Task.WhenAll(tasks);
var timeoutTask = Task.Delay(TimeSpan.FromSeconds(10));
var completed = await Task.WhenAny(allTask, timeoutTask);
if (completed == timeoutTask)
throw new TimeoutException("获取用户超时");
return (await allTask).ToList();
}Task.WhenAny 竞速
/// <summary>
/// 竞速模式 — 取最快的结果
/// </summary>
public async Task<string> FastestMirrorAsync()
{
var mirrors = new[] { "https://mirror1.com", "https://mirror2.com", "https://mirror3.com" };
var tasks = mirrors.Select(url =>
{
using var client = new HttpClient();
client.Timeout = TimeSpan.FromSeconds(5);
return client.GetStringAsync(url);
}).ToList();
// 等待任意一个完成
var firstCompleted = await Task.WhenAny(tasks);
return await firstCompleted;
}SemaphoreSlim 限流
/// <summary>
/// 限制并发数量
/// </summary>
public async Task<List<string>> DownloadWithConcurrencyAsync(List<string> urls, int maxConcurrency = 5)
{
var results = new ConcurrentBag<string>();
using var semaphore = new SemaphoreSlim(maxConcurrency);
var tasks = urls.Select(async url =>
{
await semaphore.WaitAsync();
try
{
using var client = new HttpClient();
var content = await client.GetStringAsync(url);
results.Add(content);
}
finally
{
semaphore.Release();
}
});
await Task.WhenAll(tasks);
return results.ToList();
}取消异步操作
CancellationToken
/// <summary>
/// CancellationToken 取消机制
/// </summary>
public async Task<long> CountLinesAsync(string filePath, CancellationToken ct = default)
{
long count = 0;
using var reader = new StreamReader(filePath);
while (!reader.EndOfStream)
{
ct.ThrowIfCancellationRequested(); // 检查取消
var line = await reader.ReadLineAsync();
if (line != null) count++;
}
return count;
}
// 使用超时取消
public async Task<long> CountWithTimeoutAsync(string filePath)
{
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30));
return await CountLinesAsync(filePath, cts.Token);
}
// 手动取消
public async Task RunWithCancelAsync()
{
using var cts = new CancellationTokenSource();
// 按任意键取消
_ = Task.Run(() =>
{
Console.ReadKey();
cts.Cancel();
});
try
{
var result = await CountLinesAsync("large_file.log", cts.Token);
Console.WriteLine($"共 {result} 行");
}
catch (OperationCanceledException)
{
Console.WriteLine("操作已取消");
}
}异步流
IAsyncEnumerable
/// <summary>
/// C# 8.0 异步流 — 逐个异步产生数据
/// </summary>
public async IAsyncEnumerable<User> GetUsersPagedAsync(
[EnumeratorCancellation] CancellationToken ct = default)
{
int page = 1;
while (!ct.IsCancellationRequested)
{
var users = await _userService.GetPageAsync(page++, pageSize: 100);
if (users.Count == 0) yield break;
foreach (var user in users)
{
ct.ThrowIfCancellationRequested();
yield return user;
}
}
}
// 消费异步流
public async Task ProcessUsersAsync()
{
await foreach (var user in GetUsersPagedAsync())
{
Console.WriteLine(user.Name);
}
}ConfigureAwait
上下文控制
/// <summary>
/// ConfigureAwait — 控制同步上下文
/// </summary>
// WPF/WinForms — 需要回到 UI 线程
private async void OnClick(object sender, EventArgs e)
{
var data = await GetDataAsync(); // 回到 UI 线程
label.Text = data; // 安全访问控件
}
// 库代码/后台服务 — 不需要上下文
public async Task<string> GetDataAsync()
{
// ConfigureAwait(false) 避免不必要的上下文切换,提升性能
var response = await _httpClient.GetStringAsync("/api/data")
.ConfigureAwait(false);
var result = ParseData(response); // 在任意线程执行
return result;
}常见陷阱与最佳实践
常见错误
/// <summary>
/// 异步编程常见陷阱
/// </summary>
// 1. 死锁(在 UI 线程同步等待异步方法)
public void DeadlockExample()
{
var task = GetDataAsync();
var result = task.Result; // 死锁!UI 线程被阻塞,await 无法回到 UI 线程
}
// 2. 未 await 异步方法(fire and forget 容易丢失异常)
public async Task FireAndForgetBad()
{
SendEmailAsync(); // 未 await,异常被吞掉
}
// 3. async over sync(包装同步方法为异步)
public Task<int> BadAsyncWrapper()
{
return Task.Run(() => Compute()); // 不必要的线程切换
}
// 正确做法
public int Compute() => 42; // 直接同步方法优点
缺点
总结
async/await 是 C# 异步编程的核心。关键原则:异步方法返回 Task/ValueTask,永远不要同步等待异步方法(.Result/.Wait()),库代码使用 ConfigureAwait(false),用 CancellationToken 支持取消,并行用 WhenAll,限流用 SemaphoreSlim。掌握这些模式可以编写出高效且无死锁的异步代码。
关键知识点
- 先明确这个主题影响的是语法层、运行时层,还是性能与可维护性层。
- 学习时要同时关注语言表面写法和编译器、JIT、GC 等底层行为。
- 真正有价值的是知道“为什么这样写”和“在什么边界下不能这样写”。
项目落地视角
- 把示例改成最小可运行样例,并观察编译输出、运行结果和异常行为。
- 如果它会进入团队代码规范,最好同步补充命名约定、禁用场景和替代方案。
- 涉及性能结论时,优先用 Benchmark 或实际热点链路验证,而不是凭感觉判断。
常见误区
- 只记语法糖,不知道底层成本。
- 把适用于小样例的写法直接搬到高并发或大对象场景里。
- 忽略框架版本、语言版本和运行时差异,导致结论失真。
进阶路线
- 继续向源码、IL、JIT 行为和 BCL 实现层深入。
- 把知识点和代码评审、性能诊断、面试复盘结合起来。
- 把同类主题做横向对比,例如值类型与引用类型、迭代器与 async 状态机、反射与 Source Generator。
适用场景
- 当你准备把《async/await 异步编程深入》真正落到项目里时,最适合先在一个独立模块或最小样例里验证关键路径。
- 适合在需要理解语言特性、运行时行为或 API 边界时阅读。
- 当代码开始出现性能瓶颈、可维护性问题或语义歧义时,这类主题会直接影响实现质量。
落地建议
- 先写最小可运行样例,再把结论迁移到真实业务代码。
- 同时记录这个特性的收益、限制和替代方案,避免为了“高级”而使用。
- 涉及内存、并发或序列化时,最好配合调试器或基准测试验证。
排错清单
- 先确认问题属于编译期、运行期还是语义误用。
- 检查是否存在隐式转换、装箱拆箱、闭包捕获或上下文切换等隐藏成本。
- 查看异常栈、日志和最小复现代码,优先排除使用姿势问题。
复盘问题
- 如果把《async/await 异步编程深入》放进你的当前项目,最先要验证的输入、输出和失败路径分别是什么?
- 《async/await 异步编程深入》最容易在什么规模、什么边界条件下暴露问题?你会用什么指标或日志去确认?
- 相比默认实现或替代方案,采用《async/await 异步编程深入》最大的收益和代价分别是什么?
异步状态机深入
/// <summary>
/// async/await 编译器生成的状态机
/// </summary>
// 以下代码:
public async Task<int> CalculateAsync()
{
int a = await GetValueAsync();
int b = await GetValueAsync();
return a + b;
}
// 编译器大致生成(简化版):
public struct CalculateAsyncStateMachine : IAsyncStateMachine
{
public int _state; // 当前状态
public AsyncTaskMethodBuilder<int> _builder; // 构建器
private TaskAwaiter<int> _awaiter1;
private TaskAwaiter<int> _awaiter2;
private int _a, _b; // 局部变量被提升为字段
void IAsyncStateMachine.MoveNext()
{
try
{
switch (_state)
{
case -1: // 初始状态
_awaiter1 = GetValueAsync().GetAwaiter();
if (!_awaiter1.IsCompleted)
{
_state = 0;
_builder.AwaitUnsafeOnCompleted(ref _awaiter1, ref this);
return;
}
goto case 0;
case 0: // 第一个 await 完成
_a = _awaiter1.GetResult();
_awaiter2 = GetValueAsync().GetAwaiter();
if (!_awaiter2.IsCompleted)
{
_state = 1;
_builder.AwaitUnsafeOnCompleted(ref _awaiter2, ref this);
return;
}
goto case 1;
case 1: // 第二个 await 完成
_b = _awaiter2.GetResult();
_builder.SetResult(_a + _b);
break;
}
}
catch (Exception ex)
{
_builder.SetException(ex);
}
}
void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine stateMachine) { }
}异步编程与线程池
/// <summary>
/// 异步编程与线程池的关系
/// </summary>
// async/await 不等于多线程
// await 时不占用线程,回调时可能在线程池线程上执行
// 1. I/O 密集型(推荐 async)
// await 时不占用线程,线程池线程被释放
public async Task<string> FetchDataAsync()
{
using var client = new HttpClient();
// await 期间线程被释放回线程池
var response = await client.GetStringAsync("https://api.example.com");
// 回调时从线程池取一个线程执行
return response;
}
// 2. CPU 密集型(不要用 async 包装)
// 错误做法:
public Task<int> BadCpuAsync()
{
return Task.Run(() => ComputeIntensiveWork());
}
// 正确做法:直接同步方法
public int Compute() => ComputeIntensiveWork();
// 3. 线程池饥饿问题
// 大量 async 方法同步阻塞会导致线程池饥饿
public async Task BadBlockingAsync()
{
Task.Delay(100).Wait(); // 阻塞线程池线程!
// 应该用 await Task.Delay(100)
}
// 4. 线程池配置(生产环境)
// ThreadPool.SetMinThreads 推荐设置
// 一般设置为 CPU 核心数或略高
var cpuCount = Environment.ProcessorCount;
ThreadPool.SetMinThreads(
workerThreads: cpuCount * 2,
completionPortThreads: cpuCount * 2
);异步资源释放模式
/// <summary>
/// 异步资源的正确释放
/// </summary>
// 1. IAsyncDisposable(C# 8.0+)
public class AsyncResource : IAsyncDisposable, IDisposable
{
private readonly HttpClient _client = new();
public async ValueTask DisposeAsync()
{
_client.Dispose();
await Task.Delay(10); // 模拟异步清理
Console.WriteLine("异步资源已释放");
}
public void Dispose()
{
_client.Dispose();
Console.WriteLine("同步资源已释放");
}
}
// 2. 使用 await using
public async Task ProcessAsync()
{
await using var resource = new AsyncResource();
// 使用 resource
// 方法结束时自动调用 DisposeAsync
}
// 3. 同时实现 IDisposable 和 IAsyncDisposable
// 当类同时实现两个接口时:
// - 用 using 语句调用 Dispose()
// - 用 await using 语句调用 DisposeAsync()
// - 如果只实现了 IAsyncDisposable,using 语句也会调用 DisposeAsync()
// 4. 异步工厂模式
public class DatabaseConnection
{
private DatabaseConnection() { } // 私有构造
public static async Task<DatabaseConnection> CreateAsync(string connectionString)
{
var conn = new DatabaseConnection();
await conn.OpenAsync(connectionString);
return conn;
}
private async Task OpenAsync(string connectionString)
{
// 异步打开连接
await Task.Delay(100);
}
public async ValueTask DisposeAsync()
{
// 异步关闭连接
await Task.Delay(10);
}
}
// 使用
await using var db = await DatabaseConnection.CreateAsync("conn_string");异步编程性能优化
/// <summary>
/// 异步编程性能优化技巧
/// </summary>
// 1. ValueTask — 避免热路径上的 Task 分配
public ValueTask<int> GetCachedValueAsync(string key)
{
if (_cache.TryGetValue(key, out var value))
return ValueTask.FromResult(value); // 零分配
return new ValueTask<int>(LoadValueAsync(key));
}
async Task<int> LoadValueAsync(string key)
{
await Task.Delay(100);
return 42;
}
// 注意:ValueTask 只能 await 一次!
// 错误:
// var vt = GetValueAsync();
// var r1 = await vt;
// var r2 = await vt; // 可能抛异常或返回错误结果
// 2. 避免不必要的 async/await
// 当方法体只是转发时,直接返回 Task
public Task<string> ForwardAsync() => _service.GetDataAsync();
// 而不是:
public async Task<string> ForwardAsync()
{
return await _service.GetDataAsync(); // 多一层状态机开销
}
// 3. ObjectPool 减少异步操作的内存分配
// 对于频繁创建的对象,使用对象池
var pool = ObjectPool<Buffer>.Create();
async Task ProcessAsync()
{
var buffer = pool.Get();
try
{
await FillBufferAsync(buffer);
ProcessBuffer(buffer);
}
finally
{
pool.Return(buffer);
}
}
// 4. 并行 HTTP 请求优化
public async Task<List<string>> FetchMultipleAsync(List<string> urls)
{
// 使用 HttpClientFactory(避免端口耗尽)
var client = _httpClientFactory.CreateClient();
// 并行发起所有请求
var tasks = urls.Select(url => client.GetStringAsync(url));
var results = await Task.WhenAll(tasks);
return results.ToList();
}
// 注意:不要为每个请求创建 HttpClient(端口耗尽)
// 错误做法:每次 new HttpClient()
// 正确做法:使用 IHttpClientFactory 或单例 HttpClient异步与同步混合使用的陷阱
/// <summary>
/// 异步与同步混合使用的常见陷阱
/// </summary>
// 陷阱 1: 同步方法调用异步方法
// 错误:.Result 或 .Wait() 可能死锁
public string BadSync()
{
var task = GetDataAsync();
return task.Result; // 死锁风险!
}
// 正确:使用 GetAwaiter().GetResult() 或暴露同步方法
public string SafeSync()
{
// 仍然有风险,但至少能正确传播异常
return GetDataAsync().GetAwaiter().GetResult();
}
// 最好:提供同步版本
public string GetData()
{
// 直接同步实现
return _repository.GetData();
}
// 陷阱 2: async void 事件处理器中的异常
public async void BadEventHandler(object sender, EventArgs e)
{
// 异常会导致进程崩溃
throw new InvalidOperationException("错误");
}
// 正确:用 try-catch 包裹
public async void SafeEventHandler(object sender, EventArgs e)
{
try
{
await DoWorkAsync();
}
catch (Exception ex)
{
// 处理异常或记录日志
_logger.Error(ex, "事件处理异常");
}
}
// 陷阱 3: Fire-and-forget 丢失异常
public async Task FireAndForgetBad()
{
_ = DoWorkAsync(); // 异常被吞掉
}
// 正确:使用 Task.Run + 异常处理
public void FireAndForgetSafe()
{
_ = Task.Run(async () =>
{
try
{
await DoWorkAsync();
}
catch (Exception ex)
{
_logger.Error(ex, "后台任务异常");
}
});
}
// 陷阱 4: 混合 ConfigureAwait(false)
// 库代码统一用 ConfigureAwait(false)
// UI 代码不用(需要回到 UI 线程)
// 不要在同一个调用链中混用