mcpField
D
dotnet-csharp-async-patterns

dotnet-csharp-async-patterns

github.com/wshaddix開発86127 回インストール35 週間前に更新
リポジトリ

dotnet-csharp-async-patterns について

.NET アプリケーションの非同期コードを書く開発者やコーディングエージェント向けのリファレンススキルです。async/await、キャンセル伝播、並列処理、ストリーミング、バックグラウンド処理、テストのパターンを扱います。

主な機能

  1. 01async/await の使用規則
  2. 02CancellationToken の伝播
  3. 03並列非同期処理と並列数制限
  4. 04IAsyncEnumerable によるストリーミング
  5. 05バックグラウンドサービスと非同期テスト

活用シーン

  1. 01非同期処理のブロックによるデッドロックの調査
  2. 02非同期処理へのキャンセル伝播
  3. 03独立した結果の並列読み込み
  4. 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 エージェントが非同期コードを生成する際によく犯す間違いを取り上げます。

相互参照:[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)は、Task の割り当てを避けるために ValueTask<T> を使用します。

// 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() を使用しないでください
  • 複数回 await する必要がある場合や、受け渡す場合は、.AsTask() で変換してください

エージェントが陥りやすい問題

AI エージェントが 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 メソッドは await できず、未処理の例外が発生するとプロセスがクラッシュします。

// 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 が有効なのは、フレームワークが void の戻り値型を要求するイベントハンドラー(WinForms、WPF、Blazor の @onclick)だけです。

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. エラー処理のない Fire-and-Forget
// WRONG -- exception is silently swallowed
_ = SendEmailAsync(order);

// CORRECT -- use IHostedService or a background channel
await _backgroundQueue.EnqueueAsync(ct => SendEmailAsync(order, ct));

Fire-and-Forget が本当に必要な場合でも、最低限、例外をログに記録してください。

_ = 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;
    }
}

バックグラウンド処理

バックグラウンド処理には、Task.Run や Fire-and-Forget パターンではなく、BackgroundService(または IHostedService)を使用します。登録方法については、[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 FAQ -- アプリケーションの種類ごとの ConfigureAwait の動作に関する標準的なリファレンスです。ソース:https://devblogs.microsoft.com/dotnet/configureawait-faq/

注: このスキルでは、一般公開されているガイダンスを適用しています。記載した情報源を代表したり、その見解を表明したりするものではありません。

参考資料

コメント