スレッド (Thread)

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

用語

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

以下のスレッドは、既定でそれぞれに分類されます。

  • フォアグラウンド
    • プライマリ スレッド (primary thread) または、メイン アプリケーション スレッド (main application thread)
    • Threadクラスのコンストラクタから作成されたスレッド
  • バックグラウンド
    • スレッドプール スレッド。これはランタイムで管理されているワーカースレッドのプールであり、ThreadPoolクラスで設定できる
    • アンマネージ コードからマネージ実行環境に入ったスレッド
Remarks - Thread.IsBackground Property (System.Threading) | Microsoft Learn

スレッドプール (thread pool)

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

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

参考

参考書

処理方法

非同期処理

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

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

パターン

パターン 概要 サポート
タスク ベース非同期パターン (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を実装するオブジェクトで取得  
非同期プログラミングのパターン | Microsoft Learn

タスク並列ライブラリ (Task Parallel Library : TPL)

TPLとは、System.ThreadingとSystem.Threading.Tasks名前空間にあるpublic型とAPIのセットです。タスク並列ライブラリ (TPL) - .NET | Microsoft Learn

  • System.Threading.Tasks.Task
  • System.Threading.Tasks.Parallel
  • System.Linq.ParallelEnumerable (PLINQ)

クラス階層

  • System.Object
    • System.Threading.ThreadPool
    • System.Threading.Tasks.Task
      • System.Threading.Tasks.Task<TResult>
    • System.Threading.Tasks.Parallel
    • System.Threading.Volatile
    • System.Threading.SemaphoreSlim
    • 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); // 捕捉されず、例外は失われる
}

呼び出し元では、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).)」のように表示されます。

Microsoft Learnから検索