Async/Await in C# and Task Parallelism
How async/await compiles, Task and ValueTask, CancellationToken, Task.Run and PLINQ, channels and async streams.
async/await Under the Hood
// async 方法编译成状态机
// await 处:保存状态、返回、完成后从 ThreadPool 恢复
public async Task<string> FetchAsync(string url) {
using var client = new HttpClient();
return await client.GetStringAsync(url);
}
// async 方法可返回:
// Task / Task<T> 常规
// ValueTask / ValueTask<T> 值类型,热路径省分配
// void 仅事件处理器,无法 await
// ASP.NET Core 中不需要 .Wait()/.Result
// 端到端异步,线程池就不用被同步阻塞占满
// 并发执行多个 await:Task.WhenAll
var (a, b) = await (FetchAsync(x), FetchAsync(y)); // C# 元组等待
await Task.WhenAll(jobs); // 全部完成(异常聚合)
await Task.WhenAny(jobs); // 任一完成
Mixing .Result/.Wait with async code is the classic deadlock recipe in legacy contexts — stick to await all the way down; on ASP.NET Core there is no sync context so it degrades to thread-pool starvation instead.
Cancellation and ValueTask
// CancellationToken:贯穿调用链的超时/取消令牌
public async Task<T> GetAsync(string key, CancellationToken ct = default) {
var row = await db.FindAsync(key, ct); // IO 接收令牌
return row;
}
// 组合令牌:超时 + 用户取消
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(3));
using var linked = CancellationTokenSource.CreateLinkedTokenSource(
cts.Token, userToken);
await GetAsync("k", linked.Token); // 任一触发即取消
// ValueTask:结果经常已就绪时减少分配
public ValueTask<int> CountAsync() {
return _cache.TryGetValue("n", out var n)
? new ValueTask<int>(n) // 无 Task 分配
: new ValueTask<int>(LoadAsync());
}
Task.Run, PLINQ, Channels
using System.Threading.Tasks;
using System.Threading.Channels;
// Task.Run:把 CPU 工作丢到线程池(别包已经异步的方法)
int heavy = await Task.Run(() => HeavyCompute(data));
// PLINQ:数据并行(CPU 集合处理)
double[] xs = /* ... */;
var results = xs.AsParallel()
.WithDegreeOfParallelism(8)
.Select(Transform)
.ToArray();
// Channel:生产者-消费者的异步队列
var channel = Channel.CreateBounded<Order>(100);
var writer = channel.Writer;
var reader = channel.Reader;
// 生产者
await writer.WriteAsync(order, ct);
writer.Complete();
// 消费者
await foreach (var o in reader.ReadAllAsync(ct)) {
await ProcessAsync(o);
}
Async Streams and Concurrency Limits
await foreach (var row in QueryRowsAsync(ct)) {
Console.WriteLine(row); // 逐行产生、逐行消费
}
static async IAsyncEnumerable<Row> QueryRowsAsync(
[EnumeratorCancellation] CancellationToken ct = default) {
await foreach (var batch in db.StreamBatchesAsync(ct)) {
foreach (var row in batch) yield return row;
}
}
// 限制并发数:SemaphoreSlim 或 NuGet 的 Enumerable.Chunk
using var sem = new SemaphoreSlim(8); // 最多 8 并发
var tasks = urls.Select(async url => {
await sem.WaitAsync(ct);
try { return await FetchAsync(url); }
finally { sem.Release(); }
});
var all = await Task.WhenAll(tasks);
- Avoid async void outside event handlers — exceptions crash the process and cannot be awaited.
- Use ConfigureAwait(false) in libraries so continuations do not capture a sync context; app code does not need it.
- Profile with dotnet-counters/dotnet-trace: thread pool starvation shows as growing queue length.
async/await 的内部原理
// async 方法编译成状态机
// await 处:保存状态、返回、完成后从 ThreadPool 恢复
public async Task<string> FetchAsync(string url) {
using var client = new HttpClient();
return await client.GetStringAsync(url);
}
// async 方法可返回:
// Task / Task<T> 常规
// ValueTask / ValueTask<T> 值类型,热路径省分配
// void 仅事件处理器,无法 await
// ASP.NET Core 中不需要 .Wait()/.Result
// 端到端异步,线程池就不用被同步阻塞占满
// 并发执行多个 await:Task.WhenAll
var (a, b) = await (FetchAsync(x), FetchAsync(y)); // C# 元组等待
await Task.WhenAll(jobs); // 全部完成(异常聚合)
await Task.WhenAny(jobs); // 任一完成
在传统上下文里混用 .Result/.Wait 与 async 是经典死锁配方——全程 await 才对;ASP.NET Core 无同步上下文,混用的后果退化为线程池饥饿。
取消与 ValueTask
// CancellationToken:贯穿调用链的超时/取消令牌
public async Task<T> GetAsync(string key, CancellationToken ct = default) {
var row = await db.FindAsync(key, ct); // IO 接收令牌
return row;
}
// 组合令牌:超时 + 用户取消
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(3));
using var linked = CancellationTokenSource.CreateLinkedTokenSource(
cts.Token, userToken);
await GetAsync("k", linked.Token); // 任一触发即取消
// ValueTask:结果经常已就绪时减少分配
public ValueTask<int> CountAsync() {
return _cache.TryGetValue("n", out var n)
? new ValueTask<int>(n) // 无 Task 分配
: new ValueTask<int>(LoadAsync());
}
Task.Run、PLINQ 与 Channel
using System.Threading.Tasks;
using System.Threading.Channels;
// Task.Run:把 CPU 工作丢到线程池(别包已经异步的方法)
int heavy = await Task.Run(() => HeavyCompute(data));
// PLINQ:数据并行(CPU 集合处理)
double[] xs = /* ... */;
var results = xs.AsParallel()
.WithDegreeOfParallelism(8)
.Select(Transform)
.ToArray();
// Channel:生产者-消费者的异步队列
var channel = Channel.CreateBounded<Order>(100);
var writer = channel.Writer;
var reader = channel.Reader;
// 生产者
await writer.WriteAsync(order, ct);
writer.Complete();
// 消费者
await foreach (var o in reader.ReadAllAsync(ct)) {
await ProcessAsync(o);
}
异步流与并发限制
await foreach (var row in QueryRowsAsync(ct)) {
Console.WriteLine(row); // 逐行产生、逐行消费
}
static async IAsyncEnumerable<Row> QueryRowsAsync(
[EnumeratorCancellation] CancellationToken ct = default) {
await foreach (var batch in db.StreamBatchesAsync(ct)) {
foreach (var row in batch) yield return row;
}
}
// 限制并发数:SemaphoreSlim 或 NuGet 的 Enumerable.Chunk
using var sem = new SemaphoreSlim(8); // 最多 8 并发
var tasks = urls.Select(async url => {
await sem.WaitAsync(ct);
try { return await FetchAsync(url); }
finally { sem.Release(); }
});
var all = await Task.WhenAll(tasks);
- 除事件处理器外不要 async void——异常会崩进程且无法 await。
- 类库里用 ConfigureAwait(false) 避免续体捕获同步上下文;应用层不需要。
- 用 dotnet-counters/dotnet-trace 诊断:线程池饥饿表现为队列长度持续增长。