スレッドを使用する必要があるのか、先にそれを考えます。Threads and Threading | MSDN
スレッド | 特徴 |
---|---|
フォアグラウンド スレッド (foreground thread) | マネージド実行環境を維持できる。 |
バックグラウンド スレッド (background thread) | マネージド実行環境を維持できない。つまりすべてのフォアグラウンド スレッドが停止すると、システムによってすべてのバックグラウンド スレッドが停止させられる。 |
以下のスレッドは、既定でそれぞれに分類されます。
スレッドプールとはアプリケーションから使用できる一群のスレッドのことであり、そこで処理されるスレッドには次の2種類があります。スレッド プール (C# および Visual Basic) | MSDN
スレッドによる非同期処理は、次の方法により実装できます。
スレッド セーフ (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) { this.BeginInvoke((MethodInvoker)delegate { // 呼び出し元のスレッドで、例外を再スローする throw e; }); } }).Start();
Taskクラスのスレッドから投げられた例外はその呼び出し元のスレッドでは捕捉できず、上位のドメインにも伝わらず失われます。
try
{
Task.Run(() =>
{
throw new InvalidOperationException("ERROR");
});
}
catch (Exception e)
{
Console.Write(e.Message); // 捕捉されず、例外は失われる
}
未処理の例外がある場合に実行するタスクを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が投げられる
なおContinueWith()の戻り値は、このタスクが完了したときにcontinuationOptionsの条件が満たされるならば実行されるTaskです。
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"
}
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).)」のように表示されます。