スレッドを使用する必要があるのか、先にそれを考えます。Threads and Threading | MSDN
| スレッド | 特徴 |
|---|---|
| フォアグラウンド スレッド (foreground thread) | マネージド実行環境を維持できる。 |
| バックグラウンド スレッド (background thread) | マネージド実行環境を維持できない。つまりすべてのフォアグラウンド スレッドが停止すると、システムによってすべてのバックグラウンド スレッドが停止させられる。 |
以下のスレッドは、既定でそれぞれに分類されます。
スレッドプールとはアプリケーションから使用できる一群のスレッドのことであり、そこで処理されるスレッドには次の2種類があります。スレッド プール (C# および Visual Basic) | MSDN
スレッドによる非同期処理は、次の方法により実装できます。
| パターン | 概要 | サポート |
|---|---|---|
| タスク ベース非同期パターン (Task-based Asynchronous Pattern : TAP) | メソッドにasyncキーワードを付け、TaskまたはTask<TResult>を返す | .NET Framework 4以降 |
| イベント ベース非同期パターン (Event-based Asynchronous Pattern : EAP) | MethodNameAsync()で非同期操作を開始し、MethodNameCompletedイベントでそれの結果を処理する | .NET Framework 2以降 |
| 非同期プログラミング モデル (Asynchronous Programming Model : APM) | BeginOperationName()で非同期操作を開始し、EndOperationName()で終了し、結果をIAsyncResultを実装するオブジェクトで取得 |
TPLとは、System.ThreadingとSystem.Threading.Tasks名前空間にあるpublic型とAPIのセットです。タスク並列ライブラリ (TPL) - .NET | Microsoft Learn
スレッド セーフ (thread safe) ではない型をマルチスレッドで安全に処理するには、複数のスレッドから同時にアクセスされないように対処する必要があります。その型がスレッド セーフかどうかは、.NET API ブラウザー | Microsoft Learnの解説ページで確認できます。
ThreadStatic属性を付加すると、そのフィールドはスレッドごとに異なる値となります。
[ThreadStatic] static int a = 0;ThreadStaticAttribute クラス (System) | MSDN
スレッド ローカルなデータを管理できます。ThreadLocal(T) クラス (System.Threading) | MSDN
スレッド内から投げられた例外を呼び出し元のスレッドで捕捉するには、特別な配慮が必要です。
Threadクラスで開始されたスレッド内から投げられた例外は、その呼び出し元のスレッドでは捕捉できません。
try
{
new Thread(() =>
{
throw new InvalidOperationException("ERROR");
}).Start();
}
catch (Exception e)
{
Console.Write(e.Message); // 捕捉されず、例外によってプロセスが中止させられる
}
しかし当然、スレッド内では捕捉できます。
new Thread(() =>
{
try
{
throw new InvalidOperationException("ERROR");
}
catch (Exception e)
{
Console.Write(e.Message); // 捕捉される。"ERROR"
}
}).Start();
これを利用すれば、ローカルの変数を介して例外の発生を伝えられます。
Exception exception = null;
Thread thread = new Thread(() =>
{
try
{
throw new InvalidOperationException("ERROR");
}
catch (Exception e)
{
exception = e;
}
});
thread.Start();
thread.Join(); // スレッドの終了を待機
// 例外を捕捉したならば、呼び出し元のスレッドで再スローする
if (exception != null) throw exception;
フォーム アプリケーションならば、呼び出し元のスレッドへ処理を移すことで、そこから例外を再スローできます。
new Thread(param =>
{
try
{
throw new InvalidOperationException("ERROR");
}
catch (Exception e)
{
BeginInvoke((MethodInvoker)delegate
{
// 呼び出し元のスレッドで、例外を再スローする
throw e;
});
}
}).Start();
Taskクラスのスレッドから投げられた例外はその呼び出し元のスレッドでは捕捉できず、上位のドメインにも伝わらず失われます。
try
{
Task.Run(() =>
{
throw new InvalidOperationException("ERROR");
});
}
catch (Exception e)
{
Console.Write(e.Message); // 捕捉されず、例外は失われる
}
呼び出し元では、Taskのインスタンスから例外の情報を取得できます。
Task task = Task.Run(() =>
{
throw new InvalidOperationException("ERROR");
});
Task.Delay(1).Wait();
AggregateException e1 = task.Exception; // "1 つ以上のエラーが発生しました。"
Exception e2 = task.Exception.InnerException; // "ERROR"
未処理の例外がある場合に実行するタスクをContinueWith()で指定しておけば、そこで例外を処理できます。そのとき第2引数のcontinuationOptionsにOnlyOnFaultedを指定すれば、未処理の例外が投げられた場合のみ実行されます。例外以外にも継続する処理があるならばこれを指定せず、Task.Exceptionがnullではないことから例外の発生を検知することもできます。
Task task = new Task(() =>
{
throw new InvalidOperationException("ERROR");
});
task.ContinueWith((_task) =>
{
Console.Write(_task.Exception.Message); // ここで例外を確認できる。"1 つ以上のエラーが発生しました。"
Console.Write(_task.Exception.InnerException.Message); // 実際の例外はInnerExceptionを通して取得できる。"ERROR"
}, TaskContinuationOptions.OnlyOnFaulted);
task.ContinueWith((_task) =>
{
}, TaskContinuationOptions.OnlyOnRanToCompletion);
task.Start();
task.Wait(); // 例外を捕捉したわけではないため、ここでAggregateExceptionが投げられる
Wait()かWaitAll()で待機させた場合にはAggregateException例外を捕捉することで、そこで発生したすべての例外を確認できます。他方WaitAny()、WhenAll()かWhenAny()で待機させた場合には、この例外は発生しません。
Task task = Task.Run(() =>
{
throw new InvalidOperationException("ERROR");
});
try
{
task.Wait(); // スレッドの終了を待機
}
catch (AggregateException e)
{
Console.Write(e.Message); // 捕捉される。"1 つ以上のエラーが発生しました。"
Console.Write(e.InnerException.Message); // "ERROR"
}
一方でawaitで待機させた場合には、普通に捕捉できます。非同期メソッドの例外 - try-catch - C# リファレンス | Microsoft Learn
try
{
await Task.Run(() =>
{
throw new InvalidOperationException("ERROR");
});
}
catch (Exception e)
{
// ここは呼び出し元のスレッド
Console.Write(e.Message); // 捕捉される。"ERROR"
}
これはConfigureAwait()にfalseを渡し、呼び出し元に戻さない場合も同様です。
try
{
await Task.Run(() =>
{
throw new InvalidOperationException("ERROR");
}).ConfigureAwait(false);
throw new InvalidOperationException("ERROR"); // ここで例外を投げても同じ
}
catch (Exception e)
{
// ここはTask内と同一のスレッド
Console.Write(e.Message); // 捕捉される。"ERROR"
}
Task.Start()で開始した場合は、awaitで待機させることで捕捉できます。
Task task = new Task(() =>
{
throw new InvalidOperationException("ERROR");
});
task.Start();
try
{
await task; // スレッドの終了を待機
}
catch (Exception e)
{
Console.Write(e.Message);
}
Visual Studioでは、スレッド ウィンドウでスレッドを一覧できます。そのときThreadクラスのNameプロパティからスレッド名を付けておくと、そこでの識別が容易になります。
void Callback(object state)
{
Thread thread = Thread.CurrentThread;
Console.Write(thread.ManagedThreadId); // マネージド ID (マネージド スレッドの識別番号)
Console.Write(thread.Name); // 名前 (スレッド名)
}
Threadクラス以外では、そのデリゲート内でThread.CurrentThreadから現在のThreadを取得することで、それを通してスレッド名などを設定できます。
Task task = Task.Run(() =>
{
Thread thread = Thread.CurrentThread;
thread.Name = "ThreadName";
});
出力ウィンドウで[スレッド終了メッセージ]が出力されるように設定されていると、スレッド終了時に「スレッド 0x**** はコード 0 (0x0) で終了しました。(The thread 0x**** has exited with code 0 (0x0).)」のように表示されます。