スレッド (Thread)

スレッドを使用する必要があるのか、先にそれを考えます。Threads and Threading | MSDN

用語

スレッド 特徴
フォアグラウンド スレッド (foreground thread) マネージド実行環境を維持できる。
バックグラウンド スレッド (background thread) マネージド実行環境を維持できない。つまりすべてのフォアグラウンド スレッドが停止すると、システムによってすべてのバックグラウンド スレッドが停止させられる。
Foreground and Background Threads | Microsoft Learn

スレッドプール (thread pool)

スレッドプールとはアプリケーションから使用できる一群のスレッドのことであり、そこで処理されるスレッドには次の2種類があります。スレッド プール (C# および Visual Basic) | MSDN

ワーカースレッド (worker thread)
アプリケーションがスレッドプールに対して、非同期の計算処理を要求したときに利用されるスレッド
I/Oスレッド (I/O thread)
非同期I/O処理が完了したことをアプリケーションに伝えるときに利用されるスレッド

参考

参考書

処理方法

非同期処理

スレッドによる非同期処理は、次の方法により実装できます。

  1. スレッドプールを使用し、特別な設定が不要 … ThreadPoolクラス
  2. スレッドプールを使用し、定期的に実行 … Timerクラス
  3. 細かい制御が可能 … Threadクラス

クラス階層

  • System.Object
    • System.Threading.ThreadPool
    • System.Threading.Tasks.Task
      • System.Threading.Tasks.Task<TResult>
    • System.Threading.Tasks.Parallel
    • System.Threading.Volatile
    • System.MarshalByRefObject
      • System.Threading.Timer
      • System.Threading.WaitHandle
        • System.Threading.EventWaitHandle
          • System.Threading.AutoResetEvent
          • System.Threading.ManualResetEvent
        • System.Threading.Mutex
        • System.Threading.Semaphore
      • System.ComponentModel.Component
    • System.Runtime.ConstrainedExecution.CriticalFinalizerObject

同期処理

スレッド セーフ (thread safe) ではない型をマルチスレッドで安全に処理するには、複数のスレッドから同時にアクセスされないように対処する必要があります。その型がスレッド セーフかどうかは、.NET API ブラウザー | Microsoft Learnの解説ページで確認できます。

スレッドごとのデータの管理

ThreadStatic属性

ThreadStatic属性を付加すると、そのフィールドはスレッドごとに異なる値となります。

[ThreadStatic] static int a = 0;
ThreadStaticAttribute クラス (System) | MSDN

ThreadLocal<T>クラス

スレッド ローカルなデータを管理できます。ThreadLocal(T) クラス (System.Threading) | MSDN

異なるスレッドからの例外の捕捉

Threadクラス

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クラス

Taskクラスのスレッドから投げられた例外はその呼び出し元のスレッドでは捕捉できず、上位のドメインにも伝わらず失われます。

try
{
    Task.Run(() =>
    {
        throw new InvalidOperationException("ERROR");
    });
}
catch (Exception e)
{
    Console.Write(e.Message); // 捕捉されず、例外は失われる
}

未処理の例外がある場合に実行するタスクをContinueWith()で指定しておけば、そこで例外を処理できます。

public Task ContinueWith(
    Action<Task> continuationAction,            // 実行するアクション
    TaskContinuationOptions continuationOptions // タスクの動作条件
)
ContinueWith(Action<Task>, TaskContinuationOptions) - Task.ContinueWith メソッド (System.Threading.Tasks) | Microsoft Learn

未処理の例外が投げられた場合のみ実行するようにするには、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.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).)」のように表示されます。

Microsoft Learnから検索