C#: 非同期プログラミング
1. 同期と非同期
同期型コードは、行ごとに順番に実行されます。処理に時間がかかる操作に遭遇すると、現在のスレッドをブロックし、プログラムが応答しなくなります。一方、非同期型コードは、処理に時間がかかる操作を待機している間もスレッドを解放するため、プログラムは他の処理を継続することができます。
同期ブロッキングの問題点:
- UIスレッドが占有されているため、インターフェースがフリーズし、反応しなくなっています
- サーバースレッドがI/Oの待機に費やされ、スループットが低下する
- ユーザーは待ち時間が長くなり、その結果、利用体験が低下する
非同期処理のメリット:
- I/O負荷の高い処理(ネットワーク、ファイル、データベース)は、スレッドをブロックしません
- CPU負荷の高い処理をスレッドプールにオフロードすることで、UIの応答性を維持できます
- アプリケーションの並行処理能力とスループットの向上
▶ サンプル
using System;
using System.Threading;
Console.WriteLine("Sync start: " + DateTime.Now.ToString("HH:mm:ss"));
Thread.Sleep(2000);
Console.WriteLine("Sync end: " + DateTime.Now.ToString("HH:mm:ss"));
Sync start: 10:00:00
Sync end: 10:00:02
2. タスクとタスク<T>
Taskは、結果を返さない非同期操作を表します。Task<T>は、T型の結果を返す非同期操作を表します。これらは、C#の非同期プログラミングの中核となる型です。
| 種類 | 説明 | 例 |
|---|---|---|
Task |
戻り値のない非同期操作 | Task.Delay(1000) |
Task<T> |
戻り値のある非同期操作 | Task.FromResult(42) |
▶ サンプル
using System;
using System.Threading.Tasks;
Task noResultTask = Task.Delay(500);
Task```<int>``` resultTask = Task.FromResult(42);
await noResultTask;
int value = await resultTask;
Console.WriteLine("Retrieved value: " + value);
Retrieved value: 42
3. async/await の構文
修飾子asyncは、メソッドを非同期として指定し、演算子awaitは、スレッドをブロックすることなく非同期操作が完了するのを待ちます。awaitは、asyncメソッド内でのみ使用できます。
構文規則:
asyncのメソッドは、Task、Task<T>、またはValueTask<T>を返します。awaitはメソッドの実行を一時停止し、タスクが完了するのを待ってから、処理を続行しますasyncメソッドは、最初のawaitで呼び出し元に制御を戻す
▶ サンプル
using System;
using System.Threading.Tasks;
async Task DoWorkAsync()
{
Console.WriteLine("Work start: " + DateTime.Now.ToString("HH:mm:ss.fff"));
await Task.Delay(1000);
Console.WriteLine("Work end: " + DateTime.Now.ToString("HH:mm:ss.fff"));
}
async Task```<string>``` GetMessageAsync()
{
await Task.Delay(500);
return "Async message arrived";
}
await DoWorkAsync();
string msg = await GetMessageAsync();
Console.WriteLine(msg);
Work start: 10:00:00.000
Work end: 10:00:01.000
Async message arrived
4. Task.Run
Task.Runは、CPU負荷の高い処理をスレッドプールに委譲して実行することで、現在のスレッドのブロックを回避します。計算負荷の高い操作に適していますが、I/O操作には適していません。
▶ サンプル
using System;
using System.Threading.Tasks;
int ComputeHeavy()
{
int result = 0;
for (int i = 0; i < 100_000_000; i++)
{
result += i;
}
return result;
}
Console.WriteLine("Starting computation...");
int total = await Task.Run(() => ComputeHeavy());
Console.WriteLine("Computation result: " + total);
Starting computation...
Computation result: 4999999950000000
5. Task.WhenAll / WhenAny / Delay
| 方法 | 説明 |
|---|---|
Task.WhenAll |
すべてのタスクが完了するのを待つ |
Task.WhenAny |
いずれかのタスクが完了するのを待つ |
Task.Delay |
指定されたミリ秒数だけ非同期で待機し、Thread.Sleepに置き換える |
▶ サンプル
using System;
using System.Threading.Tasks;
async Task```<string>``` FetchAsync(string site)
{
await Task.Delay(new Random().Next(500, 1500));
return site + " loaded";
}
Task```<string>``` t1 = FetchAsync("SiteA");
Task```<string>``` t2 = FetchAsync("SiteB");
Task```<string>``` t3 = FetchAsync("SiteC");
string[] all = await Task.WhenAll(t1, t2, t3);
Console.WriteLine("--- WhenAll complete ---");
foreach (string s in all)
{
Console.WriteLine(s);
}
Task```<string>``` first = await Task.WhenAny(t1, t2, t3);
Console.WriteLine("First completed: " + first.Result);
--- WhenAll complete ---
SiteA loaded
SiteB loaded
SiteC loaded
First completed: SiteA loaded
6. 非同期メソッドの命名規則
非同期メソッド名には、Asyncという接尾辞を使用する必要があります。これは、同期メソッドと非同期メソッドを区別しやすくするための .NET の規約です。
ルール:
TaskまたはTask<T>を返すメソッドには、Asyncという接尾辞を付ける必要があります- 例:
Read()→ReadAsync()、GetUser()→GetUserAsync() - イベントハンドラーは除外されます(例:
Button_Click)
▶ サンプル
using System;
using System.Threading.Tasks;
async Task```<string>``` ReadFileAsync(string path)
{
await Task.Delay(200);
return "Read: " + path;
}
async Task SaveDataAsync(string data)
{
await Task.Delay(100);
Console.WriteLine("Saved: " + data);
}
string content = await ReadFileAsync("/data.txt");
await SaveDataAsync(content);
Saved: Read: /data.txt
7. CancellationToken のキャンセルメカニズム
CancellationTokenは、非同期操作の協調的なキャンセルに使用されます。呼び出し元はCancellationTokenSourceを介してキャンセル信号を送信し、呼び出し先はトークンを確認して早期に処理を終了します。
使用手順:
CancellationTokenSourceを作成するcts.Tokenを非同期メソッドに渡す- 非同期メソッドは、定期的に
token.IsCancellationRequestedを確認するか、それをキャンセル可能なAPIに渡します。 - キャンセルが必要な場合は、
cts.Cancel()までお電話ください
▶ サンプル
using System;
using System.Threading;
using System.Threading.Tasks;
async Task LongWorkAsync(CancellationToken token)
{
for (int i = 0; i < 10; i++)
{
token.ThrowIfCancellationRequested();
Console.WriteLine("Step " + (i + 1));
await Task.Delay(300, token);
}
Console.WriteLine("All done");
}
using var cts = new CancellationTokenSource();
cts.CancelAfter(1200);
try
{
await LongWorkAsync(cts.Token);
}
catch (OperationCanceledException)
{
Console.WriteLine("Operation cancelled");
}
Step 1
Step 2
Step 3
Step 4
Operation cancelled
8. IAsyncEnumerable<T> 非同期ストリーム
C# 8 では IAsyncEnumerable<T> が導入され、データストリームの非同期生成および消費がサポートされました。await foreach を使用すると、項目を非同期で反復処理することができ、データベースのバッチ読み取りやネットワーク応答のストリーミングといったシナリオに適しています。
▶ サンプル
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
async IAsyncEnumerable```<int>``` GenerateNumbersAsync()
{
for (int i = 1; i <= 5; i++)
{
await Task.Delay(200);
yield return i;
}
}
await foreach (int num in GenerateNumbersAsync())
{
Console.WriteLine("Received: " + num);
}
Received: 1
Received: 2
Received: 3
Received: 4
Received: 5
9. 非同期処理におけるよくある落とし穴
(1) ⚠️ async void の使用は避ける
async voidはawait化できず、例外をキャッチすることもできず、アプリケーションのクラッシュを引き起こす可能性があります。イベントハンドラ内でのみ使用してください。
(2) ⚠️ .Result および .Wait() の使用は避けてください
UI や ASP.NET のコンテキストで .Result または .Wait() を呼び出すと、デッドロックが発生する可能性があります。非同期メソッドは呼び出し側がスレッドを解放するのを待ち、一方、呼び出し側は非同期メソッドが完了するのを待つため、双方が互いを待ち合う状態になります。
(3) ⚠️ ConfigureAwait(false)
ライブラリコードでは、同期コンテキストの取得を回避し、デッドロックを防ぎ、パフォーマンスを向上させるために、ConfigureAwait(false)を使用する必要があります。
▶ サンプル
using System;
using System.Threading.Tasks;
async Task```<string>``` BadMethodAsync()
{
await Task.Delay(100);
return "Done";
}
async Task SafeUsageAsync()
{
string result = await BadMethodAsync();
Console.WriteLine("Correct usage: " + result);
}
async Task LibraryMethodAsync()
{
string result = await BadMethodAsync().ConfigureAwait(false);
Console.WriteLine("Library usage: " + result);
}
await SafeUsageAsync();
await LibraryMethodAsync();
Correct usage: Done
Library usage: Done
❓ よくある質問
async void と async Task の違いは何ですか?async void は await できず、例外もキャッチできません。イベントハンドラでのみ使用すべきです。async Task が標準的なアプローチであり、await が可能で、例外も処理できます。📖 まとめ
- 同期コードはスレッドをブロックしますが、非同期コードは待機中もスレッドを解放するため、応答性とスループットが向上します
Taskは戻り値のない非同期操作を表し、Task<T>は戻り値のある非同期操作を表しますasyncは非同期メソッドを示す;awaitはブロックせずに非同期操作を待機するTask.RunCPU負荷の高い処理をスレッドプールにオフロードするTask.WhenAllはすべてが完了するのを待ち、Task.WhenAnyは最初のものが完了するのを待つTask.Delayは、Thread.Sleepの非同期版です。- 非同期メソッドには、
Asyncという接尾辞を付けるべきです CancellationTokenは協調キャンセル機能を実装しており、CancellationTokenSourceによって制御されるIAsyncEnumerable<T>+await foreachを有効にすると、非同期ストリームが有効になりますasync void、.Result、.Wait()は使用せず、ライブラリコードではConfigureAwait(false)を使用してください
📝 練習問題
Task.WhenAllを使用して3つの模擬データソースを並行してダウンロードし、すべての結果の配列を返す非同期メソッドDownloadAllAsyncを作成してください。CancellationTokenに対応し、1秒ごとに残り秒数を出力し、キャンセルされた場合は「カウントダウンがキャンセルされました」と表示する非同期のカウントダウンメソッドを作成してください。IAsyncEnumerable<int>を使用して最初の20個のフィボナッチ数を生成し、await foreachで反復処理を行い、それぞれを出力する- 以下のコードの問題点を特定し、修正してください:
async void Button_Click() { var data = GetDataAsync().Result; Console.WriteLine(data); }