C#: 非同期プログラミング

1. 同期と非同期

同期型コードは、行ごとに順番に実行されます。処理に時間がかかる操作に遭遇すると、現在のスレッドをブロックし、プログラムが応答しなくなります。一方、非同期型コードは、処理に時間がかかる操作を待機している間もスレッドを解放するため、プログラムは他の処理を継続することができます。

同期ブロッキングの問題点:

非同期処理のメリット:

▶ サンプル

CSHARP
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"));
▶ 試してみよう
TEXT 📖 参照専用
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)

▶ サンプル

CSHARP
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);
▶ 試してみよう
TEXT 📖 参照専用
Retrieved value: 42

3. async/await の構文

修飾子asyncは、メソッドを非同期として指定し、演算子awaitは、スレッドをブロックすることなく非同期操作が完了するのを待ちます。awaitは、asyncメソッド内でのみ使用できます。

構文規則:

▶ サンプル

CSHARP
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);
▶ 試してみよう
TEXT 📖 参照専用
Work start: 10:00:00.000
Work end: 10:00:01.000
Async message arrived

4. Task.Run

Task.Runは、CPU負荷の高い処理をスレッドプールに委譲して実行することで、現在のスレッドのブロックを回避します。計算負荷の高い操作に適していますが、I/O操作には適していません。

▶ サンプル

CSHARP
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);
▶ 試してみよう
TEXT 📖 参照専用
Starting computation...
Computation result: 4999999950000000

5. Task.WhenAll / WhenAny / Delay

方法 説明
Task.WhenAll すべてのタスクが完了するのを待つ
Task.WhenAny いずれかのタスクが完了するのを待つ
Task.Delay 指定されたミリ秒数だけ非同期で待機し、Thread.Sleepに置き換える

▶ サンプル

CSHARP
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);
▶ 試してみよう
TEXT 📖 参照専用
--- WhenAll complete ---
SiteA loaded
SiteB loaded
SiteC loaded
First completed: SiteA loaded

6. 非同期メソッドの命名規則

非同期メソッド名には、Asyncという接尾辞を使用する必要があります。これは、同期メソッドと非同期メソッドを区別しやすくするための .NET の規約です。

ルール:

▶ サンプル

CSHARP
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);
▶ 試してみよう
TEXT 📖 参照専用
Saved: Read: /data.txt

7. CancellationToken のキャンセルメカニズム

CancellationTokenは、非同期操作の協調的なキャンセルに使用されます。呼び出し元はCancellationTokenSourceを介してキャンセル信号を送信し、呼び出し先はトークンを確認して早期に処理を終了します。

使用手順:

  1. CancellationTokenSourceを作成する
  2. cts.Tokenを非同期メソッドに渡す
  3. 非同期メソッドは、定期的にtoken.IsCancellationRequestedを確認するか、それをキャンセル可能なAPIに渡します。
  4. キャンセルが必要な場合は、cts.Cancel()までお電話ください

▶ サンプル

CSHARP
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");
}
▶ 試してみよう
TEXT 📖 参照専用
Step 1
Step 2
Step 3
Step 4
Operation cancelled

8. IAsyncEnumerable<T> 非同期ストリーム

C# 8 では IAsyncEnumerable<T> が導入され、データストリームの非同期生成および消費がサポートされました。await foreach を使用すると、項目を非同期で反復処理することができ、データベースのバッチ読み取りやネットワーク応答のストリーミングといったシナリオに適しています。

▶ サンプル

CSHARP
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);
}
▶ 試してみよう
TEXT 📖 参照専用
Received: 1
Received: 2
Received: 3
Received: 4
Received: 5

9. 非同期処理におけるよくある落とし穴

(1) ⚠️ async void の使用は避ける

async voidawait化できず、例外をキャッチすることもできず、アプリケーションのクラッシュを引き起こす可能性があります。イベントハンドラ内でのみ使用してください。

(2) ⚠️ .Result および .Wait() の使用は避けてください

UI や ASP.NET のコンテキストで .Result または .Wait() を呼び出すと、デッドロックが発生する可能性があります。非同期メソッドは呼び出し側がスレッドを解放するのを待ち、一方、呼び出し側は非同期メソッドが完了するのを待つため、双方が互いを待ち合う状態になります。

(3) ⚠️ ConfigureAwait(false)

ライブラリコードでは、同期コンテキストの取得を回避し、デッドロックを防ぎ、パフォーマンスを向上させるために、ConfigureAwait(false)を使用する必要があります。

▶ サンプル

CSHARP
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();
▶ 試してみよう
TEXT 📖 参照専用
Correct usage: Done
Library usage: Done

❓ よくある質問

Q async voidasync Task の違いは何ですか?
A async voidawait できず、例外もキャッチできません。イベントハンドラでのみ使用すべきです。async Task が標準的なアプローチであり、await が可能で、例外も処理できます。
Q Task.Run はいつ使用すべきですか?
A CPU 負荷の高い計算処理の場合にのみ使用してください。I/O 操作については、Task.Run でラップするのではなく、ネイティブの非同期 API(ReadAsync など)を使用してください。
Q .Result を使用するとどのような問題が発生する可能性がありますか?
A UI/ASP.NET の同期コンテキストでは、デッドロックが発生する可能性があります。呼び出し元スレッドが Task の完了を待機している一方で、Task は同期コンテキストの解放を待機しているため、相互にブロックし合う状態になります。
Q ConfigureAwait(false) はいつ使用すべきですか?
A ライブラリのコード内で、同期コンテキストが捕捉されるのを防ぎ、デッドロックを回避し、パフォーマンスを向上させるために使用します。UI や ASP.NET のアプリケーション層のコードでは、通常、これを使用する必要はありません。

📖 まとめ

📝 練習問題

  1. Task.WhenAllを使用して3つの模擬データソースを並行してダウンロードし、すべての結果の配列を返す非同期メソッドDownloadAllAsyncを作成してください。
  2. CancellationTokenに対応し、1秒ごとに残り秒数を出力し、キャンセルされた場合は「カウントダウンがキャンセルされました」と表示する非同期のカウントダウンメソッドを作成してください。
  3. IAsyncEnumerable<int>を使用して最初の20個のフィボナッチ数を生成し、await foreachで反復処理を行い、それぞれを出力する
  4. 以下のコードの問題点を特定し、修正してください:async void Button_Click() { var data = GetDataAsync().Result; Console.WriteLine(data); }
Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%