C# 异步编程
00:00
async/await、Task/ValueTask、ConfigureAwait、并行编程(TPL)、Channel、IAsyncEnumerable、异步流、常见陷阱
1. async/await 基础
1.1 异步编程模型
C# 的 async/await 是语言级别的异步编程支持,让异步代码像同步代码一样直观:
// 同步方法 - 阻塞线程
public string FetchData()
{
var client = new HttpClient();
var response = client.GetStringAsync("https://api.example.com/data").Result; // 阻塞!
return response;
}
// 异步方法 - 不阻塞线程
public async Task<string> FetchDataAsync()
{
var client = new HttpClient();
var response = await client.GetStringAsync("https://api.example.com/data");
return response;
}
1.2 异步方法签名
// 返回 Task(无结果)
public async Task DoWorkAsync()
{
await Task.Delay(1000);
Console.WriteLine("工作完成");
}
// 返回 Task<T>(有结果)
public async Task<int> CalculateAsync()
{
await Task.Delay(500);
return 42;
}
// 返回 ValueTask<T>(避免分配,结果可能同步可用)
public async ValueTask<int> GetCachedValueAsync()
{
if (_cache.TryGetValue("key", out int value))
return value; // 同步返回,无堆分配
value = await ExpensiveOperationAsync();
_cache["key"] = value;
return value;
}
// 返回 void(仅事件处理器使用!)
public async void OnButtonClick(object sender, EventArgs e)
{
await LoadDataAsync(); // 异常无法被调用方捕获
}
1.3 异步流 (IAsyncEnumerable)
// C# 8+ 异步流 - 逐项异步产出
public async IAsyncEnumerable<string> StreamDataAsync(
[EnumeratorCancellation] CancellationToken ct = default)
{
for (int i = 0; i < 100; i++)
{
ct.ThrowIfCancellationRequested();
await Task.Delay(100, ct);
yield return $"数据项 {i}";
}
}
// 消费异步流
await foreach (var item in StreamDataAsync().WithCancellation(ct))
{
Console.WriteLine(item);
}
// 配置异步枚举的上下文
await foreach (var item in StreamDataAsync()
.ConfigureAwait(false))
{
// 不捕获同步上下文
}
2. Task 详解
2.1 创建与启动
// Task.Run - 在线程池上执行
Task<int> task1 = Task.Run(() =>
{
Thread.Sleep(1000);
return 42;
});
// Task.Factory.StartNew - 更多控制
Task<int> task2 = Task.Factory.StartNew(() =>
{
return ComputeHeavy();
}, CancellationToken.None,
TaskCreationOptions.LongRunning, // 长时间运行任务
TaskScheduler.Default);
// Task.CompletedTask - 已完成的空任务
return Task.CompletedTask;
// Task.FromResult - 同步结果包装为 Task
return Task.FromResult(42);
// Task.FromException / Task.FromCanceled
return Task.FromException<int>(new InvalidOperationException());
2.2 组合多个 Task
var task1 = FetchUserAsync(1);
var task2 = FetchUserAsync(2);
var task3 = FetchUserAsync(3);
// 等待所有完成
User[] users = await Task.WhenAll(task1, task2, task3);
// 等待任一完成
Task<User> first = await Task.WhenAny(task1, task2, task3);
// 超时控制
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
try
{
var result = await FetchDataAsync(cts.Token);
}
catch (OperationCanceledException)
{
Console.WriteLine("请求超时");
}
// 批量处理(控制并发数)
var tasks = ids.Select(id => FetchUserAsync(id));
var results = await Task.WhenAll(tasks);
2.3 任务链 (Continuation)
// ContinueWith - 显式任务链(不推荐,优先用 await)
task.ContinueWith(t =>
{
if (t.IsFaulted) { /* 处理异常 */ }
else { var result = t.Result; }
}, TaskContinuationOptions.OnlyOnRanToCompletion);
// 使用 await 更清晰
try
{
var result = await task;
}
catch (Exception ex)
{
// 处理异常
}
3. ConfigureAwait
// 库代码中应始终使用 ConfigureAwait(false)
public async Task<string> LibraryMethodAsync()
{
var data = await httpClient.GetStringAsync(url)
.ConfigureAwait(false); // 不捕获同步上下文
// 此后不在原始上下文执行(避免死锁)
var processed = ProcessData(data);
return processed;
}
// UI 应用中需要更新 UI 时使用默认(捕获上下文)
public async void OnButtonClick(object sender, EventArgs e)
{
var data = await FetchDataAsync(); // 捕获 UI 上下文
label.Text = data; // 安全更新 UI
}
// ASP.NET Core 无同步上下文,ConfigureAwait 无实际效果
// 但库代码仍建议使用,因为可能在其他上下文中使用
3.1 死锁场景
// WinForms/WPF 中的经典死锁
public void DeadlockExample()
{
// 同步等待异步方法 → 死锁!
var result = FetchDataAsync().Result;
// 同样会死锁
FetchDataAsync().Wait();
}
// 正确做法:一路 async 到底
public async Task CorrectExample()
{
var result = await FetchDataAsync();
}
4. 并行编程 (TPL)
4.1 Parallel 类
// Parallel.For
Parallel.For(0, 1000, i =>
{
ProcessItem(i);
});
// Parallel.ForEach
var items = Enumerable.Range(0, 1000).ToList();
Parallel.ForEach(items, item =>
{
ProcessItem(item);
});
// 带分区器控制
Parallel.ForEach(Partitioner.Create(0, 1000), range =>
{
for (int i = range.Item1; i < range.Item2; i++)
{
ProcessItem(i);
}
});
// Parallel.ForEachAsync (.NET 6+)
await Parallel.ForEachAsync(urls, new ParallelOptions
{
MaxDegreeOfParallelism = 10
}, async (url, ct) =>
{
await DownloadAsync(url, ct);
});
// Parallel.Invoke
Parallel.Invoke(
() => ComputeA(),
() => ComputeB(),
() => ComputeC()
);
4.2 ParallelLoopState 控制
Parallel.ForEach(items, (item, state) =>
{
if (item.IsInvalid)
{
state.Stop(); // 尽快停止所有迭代
return;
}
if (item.ShouldSkip)
{
state.Break(); // 停止当前项之后的迭代
return;
}
ProcessItem(item);
});
5. Channel
Channel 是高性能的异步生产者-消费者模型(.NET Core 3.0+):
using System.Threading.Channels;
// 创建 Channel
var channel = Channel.CreateBounded<string>(capacity: 100);
// var channel = Channel.CreateUnbounded<string>();
// 生产者
async Task ProduceAsync(ChannelWriter<string> writer, CancellationToken ct)
{
for (int i = 0; i < 1000; i++)
{
ct.ThrowIfCancellationRequested();
await writer.WriteAsync($"消息 {i}", ct);
}
writer.Complete(); // 标记完成
}
// 消费者
async Task ConsumeAsync(ChannelReader<string> reader, CancellationToken ct)
{
await foreach (var message in reader.ReadAllAsync(ct))
{
Console.WriteLine($"收到: {message}");
}
}
// 使用
var cts = new CancellationTokenSource();
var producerTask = ProduceAsync(channel.Writer, cts.Token);
var consumerTask = ConsumeAsync(channel.Reader, cts.Token);
await Task.WhenAll(producerTask, consumerTask);
5.1 多生产者多消费者
var channel = Channel.CreateBounded<int>(new BoundedChannelOptions(500)
{
FullMode = BoundedChannelFullMode.Wait, // 满时等待
SingleReader = false, // 多消费者
SingleWriter = false // 多生产者
});
// 启动多个生产者
var producers = Enumerable.Range(0, 3)
.Select(i => ProduceAsync(channel.Writer, i));
// 启动多个消费者
var consumers = Enumerable.Range(0, 2)
.Select(i => ConsumeAsync(channel.Reader, i));
await Task.WhenAll(
Task.WhenAll(producers),
channel.Writer.CompleteAsync().AsTask()
).ContinueWith(_ => Task.WhenAll(consumers));
6. 常见异步陷阱
6.1 陷阱与解决方案
// 陷阱1:async void(异常无法捕获)
public async void BadMethod()
{
await Task.Delay(100);
throw new Exception("崩溃!"); // 未观察到的异常
}
// 解决:返回 Task
public async Task GoodMethod()
{
await Task.Delay(100);
throw new Exception("可控异常");
}
// 陷阱2:.Result / .Wait() 死锁
var result = asyncMethod.Result;
// 解决:await
var result = await asyncMethod();
// 陷阱3:不必要的 async/await(多余的状态机)
public async Task<int> UnnecessaryAsync()
{
return await Task.FromResult(42); // 不需要 async
}
// 解决:直接返回 Task
public Task<int> BetterAsync()
{
return Task.FromResult(42);
}
// 陷阱4:忘记 await
public async Task ForgetAwaitAsync()
{
DoSomethingAsync(); // 忘记 await,异常丢失
}
// 解决:始终 await
public async Task CorrectAwaitAsync()
{
await DoSomethingAsync();
}
// 陷阱5:在循环中 await(串行执行)
foreach (var url in urls)
{
await DownloadAsync(url); // 串行,慢
}
// 解决:并行执行
await Task.WhenAll(urls.Select(url => DownloadAsync(url)));
6.2 取消模式
public async Task<long> DownloadWithProgressAsync(
string url,
IProgress<double>? progress = null,
CancellationToken ct = default)
{
using var response = await _httpClient.GetAsync(url,
HttpCompletionOption.ResponseHeadersRead, ct);
var totalBytes = response.Content.Headers.ContentLength ?? -1;
var buffer = new byte[8192];
long bytesRead = 0;
using var stream = await response.Content.ReadAsStreamAsync(ct);
int read;
while ((read = await stream.ReadAsync(buffer, ct)) > 0)
{
bytesRead += read;
progress?.Report((double)bytesRead / totalBytes);
}
return bytesRead;
}
7. 异步最佳实践
| 场景 | 建议 | 原因 |
|---|---|---|
| 库方法 | ConfigureAwait(false) | 避免死锁,提升性能 |
| 事件处理器 | async void | 唯一允许 async void 的场景 |
| 热路径同步结果 | ValueTask<T> | 避免不必要的堆分配 |
| 批量异步操作 | Task.WhenAll | 并行执行,提升吞吐 |
| 流式数据 | IAsyncEnumerable<T> | 逐项产出,降低延迟 |
| 生产消费模式 | Channel<T> | 高性能,背压支持 |
| 超时控制 | CancellationTokenSource | 可取消,可组合 |
| 进度报告 | IProgress<T> | 线程安全的进度回调 |