关于 dotnet-csharp-async-patterns
面向为 .NET 应用编写异步代码的开发者和编码 Agent,整理 async/await、取消传播及常见错误的实践。内容还涵盖并行处理、流式枚举、后台任务与异步测试。
主要功能
- 01async/await 使用规则
- 02CancellationToken 传播
- 03并行异步与有界并发
- 04IAsyncEnumerable 流式处理
- 05后台服务与异步测试
适用场景
- 01排查阻塞异步导致的死锁
- 02为异步操作传递取消信号
- 03并行加载多个独立结果
- 04使用后台服务处理队列任务
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ wshaddix/ dotnet-skills --skill dotnet-csharp-async-patterns安装后在 Claude Code 中输入/dotnet-csharp-async-patterns确认已加载。
详细介绍
dotnet-csharp-async-patterns
.NET 应用的 async/await 最佳实践。涵盖正确使用任务、传播取消,以及 AI Agent 生成异步代码时最常见的错误。
交叉参考:[skill:dotnet-csharp-dependency-injection],了解 IHostedService/BackgroundService 注册;[skill:dotnet-csharp-coding-standards],了解 Async 后缀命名;[skill:dotnet-csharp-modern-patterns],了解语言级功能。
核心规则
始终贯彻异步
异步调用链中的每个方法都必须是 async,并且要使用 await。混用同步和异步会导致死锁或线程池耗尽。
// Correct: async all the way
public async Task<Order> GetOrderAsync(int id, CancellationToken ct = default)
{
var order = await _repo.GetByIdAsync(id, ct);
return order;
}
// WRONG: blocking on async -- causes deadlocks in ASP.NET and UI contexts
public Order GetOrder(int id)
{
return _repo.GetByIdAsync(id).Result; // DEADLOCK RISK
}
优先使用 Task 和 ValueTask
默认返回 Task 或 Task<T>。如果方法经常同步完成(例如命中缓存、缓冲 I/O),则使用 ValueTask<T> 来避免分配 Task。
// ValueTask: frequently synchronous completion
public ValueTask<User?> GetCachedUserAsync(int id, CancellationToken ct = default)
{
if (_cache.TryGetValue(id, out var user))
{
return ValueTask.FromResult<User?>(user);
}
return LoadUserAsync(id, ct);
}
private async ValueTask<User?> LoadUserAsync(int id, CancellationToken ct)
{
var user = await _repo.GetByIdAsync(id, ct);
if (user is not null)
{
_cache[id] = user;
}
return user;
}
ValueTask 规则:
- 切勿对同一个
ValueTask执行多次await - 切勿对未完成的
ValueTask使用.Result或.GetAwaiter().GetResult() - 如果需要多次等待或传递它,请使用
.AsTask()进行转换
Agent 常见陷阱
以下是 AI Agent 生成 C# 代码时最常见的异步错误。
1. 阻塞异步操作(.Result、.Wait()、.GetAwaiter().GetResult())
// WRONG -- all of these can deadlock
var result = GetDataAsync().Result;
GetDataAsync().Wait();
var result = GetDataAsync().GetAwaiter().GetResult();
// CORRECT
var result = await GetDataAsync();
使用 .GetAwaiter().GetResult() 唯一安全的场景,是 C# 7.1 之前的 Main(),或少数无法使用异步的基础设施代码(静态构造函数、Dispose())。
2. async void
async void 方法无法被等待,其中未处理的异常会导致进程崩溃。
// WRONG -- fire-and-forget, unobserved exceptions
async void ProcessOrder(Order order)
{
await _repo.SaveAsync(order);
}
// CORRECT
async Task ProcessOrderAsync(Order order)
{
await _repo.SaveAsync(order);
}
async void 唯一有效的用途是事件处理程序(WinForms、WPF、Blazor @onclick),此时框架要求返回类型为 void。
3. 缺少 ConfigureAwait
在库代码中,使用 ConfigureAwait(false) 来避免捕获同步上下文。在应用代码(ASP.NET Core、控制台应用)中则不需要,因为不存在同步上下文。
// Library code
public async Task<byte[]> ReadFileAsync(string path, CancellationToken ct = default)
{
var bytes = await File.ReadAllBytesAsync(path, ct).ConfigureAwait(false);
return bytes;
}
// Application code (ASP.NET Core) -- ConfigureAwait not needed
public async Task<IActionResult> GetOrder(int id, CancellationToken ct)
{
var order = await _service.GetOrderAsync(id, ct);
return Ok(order);
}
4. 未处理异常的即发即弃操作
// WRONG -- exception is silently swallowed
_ = SendEmailAsync(order);
// CORRECT -- use IHostedService or a background channel
await _backgroundQueue.EnqueueAsync(ct => SendEmailAsync(order, ct));
如果确实必须使用即发即弃操作,至少要记录异常:
_ = Task.Run(async () =>
{
try
{
await SendEmailAsync(order);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to send email for order {OrderId}", order.Id);
}
});
5. 忘记 CancellationToken
始终接收并传递 CancellationToken。切勿静默丢弃它。
// WRONG -- token not forwarded
public async Task<List<Order>> GetAllAsync(CancellationToken ct = default)
{
return await _dbContext.Orders.ToListAsync(); // missing ct!
}
// CORRECT
public async Task<List<Order>> GetAllAsync(CancellationToken ct = default)
{
return await _dbContext.Orders.ToListAsync(ct);
}
取消模式
创建关联令牌
将外部取消与超时合并:
public async Task<Result> ProcessWithTimeoutAsync(CancellationToken ct = default)
{
using var cts = CancellationTokenSource.CreateLinkedTokenSource(ct);
cts.CancelAfter(TimeSpan.FromSeconds(30));
return await DoWorkAsync(cts.Token);
}
响应取消
public async Task ProcessBatchAsync(IEnumerable<Item> items, CancellationToken ct = default)
{
foreach (var item in items)
{
ct.ThrowIfCancellationRequested();
await ProcessItemAsync(item, ct);
}
}
并行异步
使用 Task.WhenAll 执行独立操作
public async Task<Dashboard> LoadDashboardAsync(int userId, CancellationToken ct = default)
{
var ordersTask = _orderService.GetRecentAsync(userId, ct);
var profileTask = _profileService.GetAsync(userId, ct);
var statsTask = _statsService.GetAsync(userId, ct);
await Task.WhenAll(ordersTask, profileTask, statsTask);
return new Dashboard(ordersTask.Result, profileTask.Result, statsTask.Result);
}
使用 Parallel.ForEachAsync(.NET 6+)实现有界并行
await Parallel.ForEachAsync(items, new ParallelOptions
{
MaxDegreeOfParallelism = 4,
CancellationToken = ct
}, async (item, token) =>
{
await ProcessItemAsync(item, token);
});
IAsyncEnumerable<T> 流式处理
使用 IAsyncEnumerable<T> 流式处理结果,而不是缓冲整个集合:
public async IAsyncEnumerable<Order> GetOrdersStreamAsync(
[EnumeratorCancellation] CancellationToken ct = default)
{
await foreach (var order in _dbContext.Orders.AsAsyncEnumerable().WithCancellation(ct))
{
yield return order;
}
}
后台工作
对于后台处理,请使用 BackgroundService(或 IHostedService),而不要使用 Task.Run 或即发即弃模式。有关注册模式,请参阅 [skill:dotnet-csharp-dependency-injection]。
public sealed class OrderProcessorWorker(
IServiceScopeFactory scopeFactory,
ILogger<OrderProcessorWorker> logger) : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
using var scope = scopeFactory.CreateScope();
var processor = scope.ServiceProvider.GetRequiredService<IOrderProcessor>();
await processor.ProcessPendingAsync(stoppingToken);
await Task.Delay(TimeSpan.FromSeconds(30), stoppingToken);
}
}
}
测试异步代码
[Fact]
public async Task GetOrderAsync_WhenFound_ReturnsOrder()
{
// Arrange
var repo = Substitute.For<IOrderRepository>();
repo.GetByIdAsync(42, Arg.Any<CancellationToken>())
.Returns(new Order { Id = 42 });
var service = new OrderService(repo);
// Act
var result = await service.GetOrderAsync(42);
// Assert
Assert.NotNull(result);
Assert.Equal(42, result.Id);
}
[Fact]
public async Task ProcessAsync_WhenCancelled_ThrowsOperationCanceled()
{
using var cts = new CancellationTokenSource();
cts.Cancel();
await Assert.ThrowsAsync<OperationCanceledException>(
() => _service.ProcessAsync(cts.Token));
}
知识来源
此技能中的异步模式基于以下公开内容:
- Stephen Cleary 的《Concurrency in C#》和博客——.NET 异步最佳实践的权威指南。此技能采用的主要指导原则包括:“异步到底”(绝不阻塞等待异步操作)、“没有线程”(异步 I/O 等待期间不会占用线程)、正确传递 CancellationToken、使用 IAsyncDisposable 进行异步释放,以及用于长时间运行工作的 BackgroundService 模式。来源:https://blog.stephencleary.com/
- David Fowler 的异步指南——针对 ASP.NET Core 的实用异步反模式和诊断场景。来源:https://github.com/davidfowl/AspNetCoreDiagnosticScenarios/blob/master/AsyncGuidance.md
- Stephen Toub 的 ConfigureAwait 常见问题解答——关于 ConfigureAwait 在不同应用类型中行为的权威参考。来源:https://devblogs.microsoft.com/dotnet/configureawait-faq/
**注意:**此技能采用了公开记录的指导原则,不代表也不代言上述来源。