はじめに
現代のアプリケーション開発において、非同期プログラミングは避けて通れない重要な要素となっています。特にユーザーインターフェースの応答性やサーバーリソースの効率的な利用が求められる場面では、C#のasync/await機能の適切な活用が不可欠です。
しかし、非同期プログラミングの実装には様々な落とし穴が存在します。デッドロックやメモリリーク、パフォーマンス低下など、適切な知識がないと思わぬ問題に直面することがあります。実際、Stack Overflowでは非同期プログラミングに関する質問が日々数多く投稿されており、多くの開発者がこの課題に直面していることがわかります。
本記事では、C#におけるasync/awaitの基礎から実践的な使用方法まで、体系的に解説していきます。
async/awaitの基本的な仕組みと動作原理
非同期プログラミングの7つの重要な実装原則
パフォーマンス最適化のための具体的なテクニック
WebAPI、データベース、ファイル操作での実践的な実装方法
デッドロックやメモリリークなど、よくあるエラーの解決方法
非同期処理のパフォーマンスモニタリングと最適化手法
実践的なレート制限やキャンセレーション処理の実装方法
この記事を読み終えた後には、async/awaitを効果的に活用し、パフォーマンスと信頼性の高い非同期処理を実装できるようになることを目指しています。初級者から中級者まで、それぞれのレベルに応じた知識と実践的なテクニックを提供していきます。
それでは、C# asyncの世界を詳しく見ていきましょう。
なぜ非同期プログラミングが重要なのか
非同期プログラミングの重要性は、現代のアプリケーション開発において年々増加しています。その主な理由は以下の3つに集約されます。
1. アプリケーションの応答性向上
同期処理では、時間のかかる操作(I/O処理など)が完了するまでアプリケーション全体がブロックされてしまいます。
これは特にUIアプリケーションで致命的な問題となります。
例えば、以下のような同期的なコードを考えてみましょう。
// 同期処理の例 - UIがフリーズする可能性がある private void LoadDataButton_Click(object sender, EventArgs e) { var data = LoadLargeDataFile(); // 数秒かかる処理 DisplayData(data); // UIの更新 }
これを非同期処理に書き換えることで、UIの応答性を維持できます。
// 非同期処理の例 - UIの応答性が維持される private async void LoadDataButton_Click(object sender, EventArgs e) { var data = await LoadLargeDataFileAsync(); // バックグラウンドで実行 DisplayData(data); // UIの更新 }
2. システムリソースの効率的な利用
非同期プログラミングを適切に実装することで、システムリソースを最大限に活用できます。
特にWebアプリケーションやマイクロサービスでは、この効果が顕著に現れます。
以下は、同期処理と非同期処理でのリソース使用効率の比較です。
処理方式 | 同時接続数 | メモリ使用量 | スレッド使用数 | レスポンスタイム |
---|---|---|---|---|
同期処理 | 100 | 高い | 100+ | 遅い |
非同期処理 | 100 | 低い | 数個 | 速い |
3. スケーラビリティの向上
クラウドネイティブな環境では、アプリケーションの水平スケーリング能力が重要です。
非同期プログラミングは、この要件を満たすための重要な要素となります。
典型的な例として、WebAPIのデータベースアクセスを考えてみましょう。
// スケーラビリティの低い同期処理の例 public class ProductController : ControllerBase { private readonly IProductRepository _repository; public IActionResult GetProduct(int id) { var product = _repository.GetById(id); // データベースをブロッキング return Ok(product); } } // スケーラビリティの高い非同期処理の例 public class ProductController : ControllerBase { private readonly IProductRepository _repository; public async Task<IActionResult> GetProduct(int id) { var product = await _repository.GetByIdAsync(id); // 非ブロッキング return Ok(product); } }
非同期処理を実装することで、以下のような具体的なメリットが得られます。
- リクエスト処理能力の向上
- 同じハードウェアリソースでより多くのリクエストを処理可能
- レスポンスタイムの安定化
- システムの安定性向上
- リソース枯渇のリスク低減
- デッドロックの防止
- 例外処理の適切な制御
- コスト効率の改善
- サーバーリソースの効率的な利用
- スケールアウトの必要性減少
このように、非同期プログラミングは現代のアプリケーション開発において、パフォーマンス、スケーラビリティ、そしてユーザー体験の向上に直接的に寄与する重要な要素となっています。
C# asyncの基礎知識
async/awaitの仕組みと動作原理
async/awaitは、C#で非同期プログラミングを実装するための言語機能です。この機能により、複雑な非同期処理をシンプルに記述できるようになりました。
基本的な構文と動作
// 基本的な非同期メソッドの構造 public async Task<string> GetDataAsync() { // 非同期操作の前の処理 var client = new HttpClient(); // awaitで非同期処理の完了を待機 var result = await client.GetStringAsync("https://api.example.com/data"); // 非同期操作の後の処理 return result.ToUpper(); } // 非同期メソッドの呼び出し public async Task ProcessDataAsync() { string data = await GetDataAsync(); // 非同期メソッドの待機 Console.WriteLine(data); }
内部動作の解説
- 状態機械への変換
- コンパイラは非同期メソッドを状態機械に変換
- 各awaitポイントが状態遷移のタイミングとなる
- コンテキストの保持
- 実行コンテキスト(例:UI Thread)が自動的に管理される
- awaitの後で元のコンテキストに戻る
// 非同期処理の状態遷移を理解するための例 public async Task StateTransitionExampleAsync() { // 1. Pending状態 var task = LongRunningOperationAsync(); // 2. Running状態 await task; // 実行中 // 3. Completed/Faulted/Canceled状態 try { var result = await task; // Completed状態 } catch (OperationCanceledException) { // Canceled状態 } catch (Exception) { // Faulted状態 } }
同期処理と非同期処理の違い
両者の主な違いを実践的な例で見てみましょう。
// 同期処理の例 public string GetDataSync() { Thread.Sleep(1000); // I/O操作を模擬 return "Data"; } // 非同期処理の例 public async Task<string> GetDataAsync() { await Task.Delay(1000); // 非同期のI/O操作を模擬 return "Data"; }
特性 | 同期処理 | 非同期処理 |
---|---|---|
スレッドの利用 | ブロッキング | ノンブロッキング |
リソース効率 | 低い | 高い |
コードの複雑さ | シンプル | やや複雑 |
デバッグ | 容易 | より難しい |
エラー処理 | 直感的 | 注意が必要 |
Task、Task、ValueTaskの使い分け
これらの型は、非同期操作の結果を表現する手段として提供されています。
Task
// 戻り値のない非同期処理 public async Task SendNotificationAsync() { await Task.Delay(100); // 何らかの非同期処理 Console.WriteLine("通知送信完了"); }
Task<T>
// 値を返す非同期処理 public async Task<int> CalculateAsync() { await Task.Delay(100); // 計算処理を模擬 return 42; }
ValueTask<T>
// キャッシュを活用する非同期処理 public ValueTask<int> GetCachedValueAsync() { if (_cache.TryGetValue("key", out int value)) { return new ValueTask<int>(value); // キャッシュヒット時は即時返却 } return new ValueTask<int>(LoadValueAsync()); // キャッシュミス時は非同期読み込み }
- Task
- 一般的な非同期操作
- 戻り値が不要な場合
- メモリ効率を特に考慮しない場合
- Task<T>
- 値を返す必要がある非同期操作
- 汎用的な用途
- 複数の非同期操作を組み合わせる場合
- ValueTask<T>
- 高頻度で呼び出される非同期API
- キャッシュを活用する処理
- メモリ割り当てを最小限に抑えたい場合
これらの基礎知識を踏まえた上で、次のセクションでは具体的なベストプラクティスについて解説していきます。
非同期プログラミングの7つの鉄則
1. ConfigureAwaitの適切な使用方法
ConfigureAwaitは、非同期処理の文脈で最も重要な最適化手法の1つです。
// 悪い例:デフォルトの動作 public async Task UpdateUIAsync() { var data = await GetDataAsync(); // 元のコンテキストに戻る UpdateUI(data); // UI更新のためにコンテキストが必要 } // 良い例:必要な場合のみコンテキストを維持 public async Task ProcessDataAsync() { // UI操作を含まない処理ではコンテキスト復帰を抑制 var data = await GetDataAsync().ConfigureAwait(false); var processed = await ProcessAsync(data).ConfigureAwait(false); // UI操作が必要な場合は最後でコンテキストを維持 await UpdateUIAsync(); // ConfigureAwait(false)を使用しない }
UIスレッドが不要な処理ではConfigureAwait(false)
を使用
ライブラリコードでは基本的にConfigureAwait(false)
を使用
UI操作を行う場合はConfigureAwait(false)
を使用しない
2. 例外処理の正しい実装方法
非同期処理での例外処理は、同期処理とは異なる注意点があります。
// 包括的な例外処理の実装例 public async Task<DataResult> FetchDataWithRetriesAsync() { int retryCount = 3; for (int i = 0; i < retryCount; i++) { try { return await FetchDataAsync().ConfigureAwait(false); } catch (HttpRequestException ex) { if (i == retryCount - 1) throw; // 最後の試行で失敗した場合は例外を再スロー await Task.Delay(1000 * (i + 1)).ConfigureAwait(false); // 待機時間を増やしながらリトライ Logger.LogWarning($"Retry {i + 1}/{retryCount} after error: {ex.Message}"); } } throw new TimeoutException("All retry attempts failed"); }
適切な粒度で例外をキャッチ
リトライロジックの実装
エラーログの記録
適切な例外の変換と伝播
3. デッドロックを防ぐための対策
デッドロックは非同期プログラミングにおける重大な問題の1つです。
// デッドロックを引き起こす可能性のあるコード public class DeadlockExample { // 悪い例:同期的に非同期メソッドを待機 public void DoWork() { var task = GetDataAsync(); task.Wait(); // デッドロックの可能性 } private async Task<string> GetDataAsync() { await Task.Delay(1000); return "Data"; } } // 改善例:非同期チェーンを維持 public class DeadlockSolution { // 良い例:非同期性を保持 public async Task DoWorkAsync() { var data = await GetDataAsync(); // 処理続行 } private async Task<string> GetDataAsync() { await Task.Delay(1000).ConfigureAwait(false); return "Data"; } }
非同期メソッドを同期的に待機しない
非同期チェーンを最上位まで維持
ConfigureAwait(false)
の適切な使用
4. 非同期メソッドの命名規則
適切な命名は、コードの可読性と保守性を高めます。
public class NamingExample { // 良い例:非同期メソッドの命名 public async Task<User> GetUserAsync(int userId) { return await _repository.GetUserAsync(userId); } // 良い例:イベントハンドラーの非同期メソッド private async void OnUserDataReceived(object sender, UserDataEventArgs e) { await ProcessUserDataAsync(e.Data); } }
メソッド名の末尾にAsync
サフィックスを付ける
イベントハンドラーは例外としてasync void
を許容
対応する同期メソッドがある場合は命名を統一
5. キャンセレーショントークンの活用
長時間実行される非同期処理では、キャンセル機能の実装が重要です。
public class CancellationExample { // キャンセレーショントークンを受け取る非同期メソッド public async Task<string> LongRunningOperationAsync(CancellationToken cancellationToken) { try { // キャンセルチェック cancellationToken.ThrowIfCancellationRequested(); // キャンセル可能な待機 await Task.Delay(5000, cancellationToken); // 時間のかかる処理 for (int i = 0; i < 100; i++) { // 定期的なキャンセルチェック if (i % 10 == 0) cancellationToken.ThrowIfCancellationRequested(); await Task.Delay(100, cancellationToken); } return "Operation completed"; } catch (OperationCanceledException) { // キャンセル時の後処理 await CleanupAsync(); throw; // 例外を再スロー } } }
適切な間隔でのキャンセルチェック
リソースの適切なクリーンアップ
キャンセル時の状態管理
6. 並列処理との組み合わせ方
非同期処理と並列処理を適切に組み合わせることで、パフォーマンスを最大化できます。
public class ParallelAsyncExample { // 並列非同期処理の実装例 public async Task ProcessItemsAsync(IEnumerable<string> items) { // 並列度の制御 var parallelOptions = new ParallelOptions { MaxDegreeOfParallelism = Environment.ProcessorCount }; // 並列非同期処理の実行 var tasks = items.Select(async item => { await Task.Delay(100); // 何らかの非同期処理 return await ProcessItemAsync(item); }); // 全てのタスクの完了を待機 var results = await Task.WhenAll(tasks); } }
適切な並列度の設定
リソース消費の管理
エラーハンドリング
7. メモリリークを防ぐためのベストプラクティス
非同期処理でのメモリリークは、特に注意が必要な問題です。
public class MemoryLeakPrevention : IDisposable { private readonly CancellationTokenSource _cts = new CancellationTokenSource(); private bool _disposed; // メモリリークを防ぐ実装例 public async Task StartProcessingAsync() { try { while (!_disposed) { await ProcessDataAsync(_cts.Token); await Task.Delay(1000, _cts.Token); } } catch (OperationCanceledException) when (_disposed) { // 正常な終了 } } public void Dispose() { if (!_disposed) { _disposed = true; _cts.Cancel(); _cts.Dispose(); } } }
リソースの適切な破棄
キャンセレーショントークンの管理
イベントハンドラーの解除
循環参照の回避
これらの7つの原則を適切に実装することで、信頼性が高く、パフォーマンスの良い非同期プログラムを開発することができます。
パフォーマンス最適化のテクニック
非同期処理のボトルネック特定方法
非同期処理のパフォーマンスを最適化するには、まずボトルネックを正確に特定する必要があります。
// パフォーマンス計測を含む非同期処理の例 public class PerformanceMonitoring { public async Task<PerformanceReport> MeasureOperationAsync() { var stopwatch = Stopwatch.StartNew(); var metrics = new Dictionary<string, TimeSpan>(); try { // データベース操作の計測 var dbTimer = Stopwatch.StartNew(); var data = await FetchDataFromDatabaseAsync(); metrics["Database"] = dbTimer.Elapsed; // API呼び出しの計測 var apiTimer = Stopwatch.StartNew(); var apiResult = await CallExternalApiAsync(); metrics["API"] = apiTimer.Elapsed; // 処理の計測 var processTimer = Stopwatch.StartNew(); var result = await ProcessDataAsync(data, apiResult); metrics["Processing"] = processTimer.Elapsed; return new PerformanceReport { TotalTime = stopwatch.Elapsed, OperationMetrics = metrics }; } finally { stopwatch.Stop(); } } }
詳細な時間計測の実装
各処理段階の分離
メモリ使用量のモニタリング
ValueTaskを使用したメモリ最適化
ValueTaskは、特定のシナリオでメモリアロケーションを削減できます。
public class CacheOptimization { private readonly Dictionary<string, string> _cache = new(); private readonly SemaphoreSlim _semaphore = new(1); // ValueTaskを使用したキャッシュ最適化の例 public ValueTask<string> GetCachedDataAsync(string key) { // キャッシュヒット時はアロケーションなし if (_cache.TryGetValue(key, out var cachedValue)) { return new ValueTask<string>(cachedValue); } // キャッシュミス時のみTaskを生成 return new ValueTask<string>(LoadAndCacheDataAsync(key)); } private async Task<string> LoadAndCacheDataAsync(string key) { try { await _semaphore.WaitAsync(); // double-check pattern if (_cache.TryGetValue(key, out var cachedValue)) { return cachedValue; } var data = await FetchDataFromSourceAsync(key); _cache[key] = data; return data; } finally { _semaphore.Release(); } } }
キャッシュヒット時のアロケーション削減
適切なスレッド同期の実装
キャッシュ戦略の最適化
非同期ストリーム処理の実装
大量のデータを効率的に処理する場合、非同期ストリームが有効です。
public class StreamProcessing { // 非同期ストリームを使用したデータ処理 public async IAsyncEnumerable<ProcessedData> ProcessLargeDataSetAsync( [EnumeratorCancellation] CancellationToken cancellationToken = default) { await using var context = new DataContext(); // データベースからストリーミング var query = context.LargeDataSet .AsAsyncEnumerable() .WithCancellation(cancellationToken); await foreach (var item in query) { if (cancellationToken.IsCancellationRequested) break; // メモリ効率の良い処理 var processed = await ProcessItemAsync(item); yield return processed; } } // ストリーム処理の使用例 public async Task ConsumeStreamAsync() { await foreach (var data in ProcessLargeDataSetAsync()) { // バッファリングなしでデータを処理 await SendToClientAsync(data); } } }
メモリ使用量の制御
バッファリングの最適化
キャンセレーション対応
パフォーマンス最適化のベストプラクティス
1. バッチ処理の最適化
// 効率的なバッチ処理の実装 public async Task ProcessBatchAsync<T>(IEnumerable<T> items, int batchSize = 100) { var batch = new List<T>(batchSize); foreach (var item in items) { batch.Add(item); if (batch.Count >= batchSize) { await ProcessBatchInternalAsync(batch); batch.Clear(); } } if (batch.Count > 0) { await ProcessBatchInternalAsync(batch); } }
2. 並列度の最適化
// 並列度を制御した処理 public async Task ProcessWithOptimalParallelismAsync(IEnumerable<string> items) { var semaphore = new SemaphoreSlim(Environment.ProcessorCount); var tasks = new List<Task>(); foreach (var item in items) { await semaphore.WaitAsync(); tasks.Add(Task.Run(async () => { try { await ProcessItemAsync(item); } finally { semaphore.Release(); } })); } await Task.WhenAll(tasks); }
3. メモリ使用量の最適化
// メモリ効率の良いストリーム処理 public async Task ProcessLargeFileAsync(string filePath) { using var fileStream = File.OpenRead(filePath); using var reader = new StreamReader(fileStream); string line; var buffer = new List<string>(1000); while ((line = await reader.ReadLineAsync()) != null) { buffer.Add(line); if (buffer.Count >= 1000) { await ProcessBufferAsync(buffer); buffer.Clear(); } } if (buffer.Count > 0) { await ProcessBufferAsync(buffer); } }
これらの最適化テクニックを適切に組み合わせることで、効率的で高性能な非同期アプリケーションを実現できます。
実践的なユースケース
WebAPIでの非同期処理の実装
WebAPIでの非同期処理は、スケーラビリティとパフォーマンスの要となります。
[ApiController] [Route("api/[controller]")] public class OrderController : ControllerBase { private readonly IOrderService _orderService; private readonly ILogger<OrderController> _logger; // 複数のデータソースを扱う非同期API [HttpGet("details/{orderId}")] public async Task<ActionResult<OrderDetailsDto>> GetOrderDetailsAsync(int orderId) { try { // 並列で複数の非同期処理を実行 var orderTask = _orderService.GetOrderAsync(orderId); var customerTask = _orderService.GetCustomerDetailsAsync(orderId); var itemsTask = _orderService.GetOrderItemsAsync(orderId); // 全ての処理の完了を待機 await Task.WhenAll(orderTask, customerTask, itemsTask); return Ok(new OrderDetailsDto { Order = await orderTask, Customer = await customerTask, Items = await itemsTask }); } catch (OrderNotFoundException) { return NotFound(); } catch (Exception ex) { _logger.LogError(ex, "Error fetching order details"); return StatusCode(500); } } // ストリーミングを使用した大量データの処理 [HttpGet("export")] public async Task<IActionResult> ExportOrdersAsync( [FromQuery] DateTime startDate, [FromQuery] DateTime endDate) { Response.ContentType = "text/csv"; Response.Headers.Add("Content-Disposition", "attachment; filename=orders.csv"); await foreach (var orderLine in _orderService.GetOrdersAsStreamAsync(startDate, endDate)) { var csvLine = ConvertToCsv(orderLine); await Response.WriteAsync(csvLine); await Response.Body.FlushAsync(); } return new EmptyResult(); } }
データベースアクセスの非同期化
Entity Frameworkを使用した効率的な非同期データアクセスの実装例を示します。
public class OrderRepository : IOrderRepository { private readonly OrderDbContext _context; // 効率的な非同期クエリの実装 public async Task<IReadOnlyList<Order>> GetRecentOrdersAsync( int customerId, int count = 10, CancellationToken cancellationToken = default) { return await _context.Orders .Include(o => o.Items) .Where(o => o.CustomerId == customerId) .OrderByDescending(o => o.OrderDate) .Take(count) .AsNoTracking() // 読み取り専用の場合はトラッキング無効化 .ToListAsync(cancellationToken); } // バッチ処理を使用した更新 public async Task UpdateOrderStatusAsync( IEnumerable<int> orderIds, OrderStatus newStatus, CancellationToken cancellationToken = default) { const int batchSize = 100; var batches = orderIds.Chunk(batchSize); foreach (var batch in batches) { await _context.Orders .Where(o => batch.Contains(o.Id)) .ExecuteUpdateAsync(s => s.SetProperty(o => o.Status, newStatus), cancellationToken); await _context.SaveChangesAsync(cancellationToken); } } // 非同期トランザクションの実装 public async Task<bool> ProcessOrderAsync( Order order, CancellationToken cancellationToken = default) { using var transaction = await _context.Database .BeginTransactionAsync(cancellationToken); try { // 在庫チェック var items = await ValidateInventoryAsync(order.Items, cancellationToken); if (!items.All(i => i.IsAvailable)) return false; // 在庫の更新 await UpdateInventoryAsync(order.Items, cancellationToken); // 注文の保存 _context.Orders.Add(order); await _context.SaveChangesAsync(cancellationToken); await transaction.CommitAsync(cancellationToken); return true; } catch { await transaction.RollbackAsync(cancellationToken); throw; } } }
ファイル操作の非同期処理
大容量ファイルを効率的に処理する実装例を示します。
public class FileProcessor { // 大容量ファイルの非同期読み取り public async Task ProcessLargeFileAsync( string filePath, Func<string, Task> lineProcessor, CancellationToken cancellationToken = default) { const int bufferSize = 4096; using var fileStream = new FileStream( filePath, FileMode.Open, FileAccess.Read, FileShare.Read, bufferSize, FileOptions.Asynchronous | FileOptions.SequentialScan); using var reader = new StreamReader(fileStream); string line; while ((line = await reader.ReadLineAsync()) != null) { cancellationToken.ThrowIfCancellationRequested(); await lineProcessor(line); } } // 並列ファイル処理の実装 public async Task ProcessMultipleFilesAsync( IEnumerable<string> filePaths, Func<string, Task> fileProcessor, int maxConcurrency = 3) { using var semaphore = new SemaphoreSlim(maxConcurrency); var tasks = new List<Task>(); foreach (var filePath in filePaths) { await semaphore.WaitAsync(); tasks.Add(Task.Run(async () => { try { await fileProcessor(filePath); } finally { semaphore.Release(); } })); } await Task.WhenAll(tasks); } // 非同期ファイル書き込み public async Task ExportDataAsync<T>( IAsyncEnumerable<T> data, string outputPath, CancellationToken cancellationToken = default) { const int bufferSize = 4096; using var fileStream = new FileStream( outputPath, FileMode.Create, FileAccess.Write, FileShare.None, bufferSize, FileOptions.Asynchronous | FileOptions.SequentialScan); using var writer = new StreamWriter(fileStream); await foreach (var item in data.WithCancellation(cancellationToken)) { var line = SerializeItem(item); await writer.WriteLineAsync(line); // 定期的なフラッシュ if (fileStream.Position > bufferSize) { await writer.FlushAsync(); } } } }
これらの実装例は、実際のプロジェクトですぐに活用できる実践的なパターンを示しています。
よくあるエラーとその解決方法
デッドロックの検出と解決
デッドロックは非同期プログラミングにおける最も一般的な問題の1つです。
一般的なデッドロックパターン
// デッドロックを引き起こす典型的なパターン public class DeadlockPatterns { // パターン1: 同期的な待機 public string GetDataWrong() { // 💥 デッドロックの危険性 return GetDataAsync().Result; } // パターン2: ASP.NET CoreでのContext不一致 public async Task<string> GetDataWithContextIssue() { return await GetDataAsync(); // コンテキストの切り替えで問題発生 } } // デッドロックを回避する正しい実装 public class DeadlockSolutions { // 解決策1: 非同期チェーンの維持 public async Task<string> GetDataCorrect() { return await GetDataAsync().ConfigureAwait(false); } // 解決策2: 同期APIが必要な場合 public string GetDataSyncCorrect() { // 新しいスレッドで実行して同期的に待機 return Task.Run(async () => await GetDataAsync()) .GetAwaiter() .GetResult(); } }
デッドロック検出のためのツール
public static class DeadlockDetector { public static async Task DetectPotentialDeadlocksAsync( Func<Task> action, TimeSpan timeout) { using var cts = new CancellationTokenSource(timeout); try { await Task.WhenAny( action(), Task.Delay(timeout, cts.Token) ); if (!cts.IsCancellationRequested) { // 正常終了 return; } throw new TimeoutException("Potential deadlock detected"); } catch (TaskCanceledException) { throw new TimeoutException("Potential deadlock detected"); } } }
例外処理の落とし穴
非同期プログラミングにおける例外処理には特有の課題があります。
public class ExceptionHandlingPatterns { // 悪い例: 失われる例外 public async void FireAndForgetWrong() { await Task.Run(() => throw new Exception("Lost exception")); } // 良い例: 例外をキャッチして適切に処理 public async Task FireAndForgetCorrect() { try { await Task.Run(() => throw new Exception("Caught exception")) .ConfigureAwait(false); } catch (Exception ex) { // 例外をログに記録 Logger.LogError(ex, "Error in async operation"); throw; // 必要に応じて再スロー } } // 複数の例外を適切に処理する例 public async Task HandleMultipleExceptionsAsync() { var tasks = new List<Task>(); try { // 複数のタスクを実行 tasks.Add(Task1Async()); tasks.Add(Task2Async()); await Task.WhenAll(tasks); } catch (Exception ex) { // 全ての例外を取得 var exceptions = tasks .Where(t => t.IsFaulted) .SelectMany(t => t.Exception?.InnerExceptions ?? Array.Empty<Exception>()) .ToList(); // 例外を適切に処理 foreach (var exception in exceptions) { Logger.LogError(exception, "Task failed"); } throw new AggregateException(exceptions); } } }
メモリリークの防止方法
非同期処理でのメモリリークは注意深い実装が必要です。
public class MemoryLeakPrevention : IDisposable { private readonly CancellationTokenSource _cts = new(); private readonly List<IDisposable> _disposables = new(); private bool _disposed; // メモリリークを防ぐための実装例 public async Task StartLongRunningProcessAsync() { if (_disposed) throw new ObjectDisposedException(nameof(MemoryLeakPrevention)); try { // リソースの登録 using var scope = RegisterDisposable(new SomeResource()); while (!_cts.Token.IsCancellationRequested) { await ProcessDataAsync(_cts.Token); await Task.Delay(1000, _cts.Token); } } catch (OperationCanceledException) when (_disposed) { // 正常な終了 } } // イベントハンドラの適切な管理 private EventHandler<DataReceivedEventArgs> _handler; public void SubscribeToEvents(DataSource source) { _handler = async (s, e) => await HandleDataReceivedAsync(e); source.DataReceived += _handler; // イベントハンドラの解除を登録 RegisterDisposable(new EventUnsubscriber(source, _handler)); } private IDisposable RegisterDisposable(IDisposable disposable) { if (_disposed) { disposable.Dispose(); throw new ObjectDisposedException(nameof(MemoryLeakPrevention)); } _disposables.Add(disposable); return new DisposableWrapper(disposable, () => _disposables.Remove(disposable)); } public void Dispose() { if (_disposed) return; _disposed = true; _cts.Cancel(); foreach (var disposable in _disposables.ToArray()) { try { disposable.Dispose(); } catch (Exception ex) { Logger.LogError(ex, "Error disposing resource"); } } _disposables.Clear(); _cts.Dispose(); } } // イベント購読解除用のヘルパークラス public class EventUnsubscriber : IDisposable { private readonly DataSource _source; private readonly EventHandler<DataReceivedEventArgs> _handler; public EventUnsubscriber( DataSource source, EventHandler<DataReceivedEventArgs> handler) { _source = source; _handler = handler; } public void Dispose() { _source.DataReceived -= _handler; } }
これらのエラーパターンを理解し、適切な解決策を実装することで、より信頼性の高い非同期プログラムを作成できます。
asyncのまとめ
C#の非同期プログラミングは、適切な実装により大幅なパフォーマンス向上が見込める一方で、誤った実装は深刻な問題を引き起こす可能性があります。
本記事で解説した実装パターンとベストプラクティスを活用することで、信頼性が高く効率的な非同期処理を実現できます。
- 基礎の重要性
- 非同期処理の状態遷移の理解
- Task、
Task<T>
、ValueTaskの適切な使い分け - ConfigureAwaitの正しい使用
- パフォーマンス最適化
- メモリ使用量の最適化
- 並列処理との効果的な組み合わせ
- 非同期ストリーム処理の活用
- エラー処理とデバッグ
- デッドロックの検出と防止
- 適切な例外処理パターン
- メモリリークの防止策
- 実装のベストプラクティス
- キャンセレーショントークンの活用
- レート制限の実装
- パフォーマンスモニタリング
これらのポイントを押さえることで、実践的な非同期プログラミングのスキルを習得し、高品質なアプリケーション開発が可能になります。