Async/Await in C# and Task Parallelism

How async/await compiles, Task and ValueTask, CancellationToken, Task.Run and PLINQ, channels and async streams.

AdvancedUpdated 2026-10-2951K views

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.

Friends & Partners