スレッド (Thread)

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

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

非同期処理

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

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

クラス階層

  • System.Object
    • System.Threading.ThreadPool
    • System.MarshalByRefObject
    • System.Runtime.ConstrainedExecution.CriticalFinalizerObject
    • System.Threading.Tasks.Task
      • System.Threading.Tasks.Task<TResult>

ThreadPoolクラス

スレッドプールにキューを置くには、QueueUserWorkItem()メソッドを使用します。

public static bool QueueUserWorkItem(
    WaitCallback callBack,   // 実行するメソッド
    Object state             // メソッドへ渡すデータ
)
ThreadPool.QueueUserWorkItem メソッド (System.Threading) | MSDN

callback引数で渡したメソッドは、スレッドプールのスレッドが使用可能になったときに実行されます。stateを省略したときは、nullが渡されます。

スレッドで実行するメソッドはWaitCallbackデリゲート型である必要があり、次の形式となります。

delegate void WaitCallback( Object state )
WaitCallback デリゲート (System.Threading) | MSDN

サンプルコード

void Main()
{
    System.Threading.ThreadPool.QueueUserWorkItem( MethodName, 123 );
}

void MethodName( Object state )
{
    // このメソッドが制御を戻すと、スレッドはスレッドプールに戻り次のタスクを待つ
}

このコードは匿名メソッドを用いることで、

System.Threading.ThreadPool.QueueUserWorkItem(
    delegate( Object state )
    {
        // 別スレッドでの処理
    }, 123 );

のようにも、ラムダ式を用いることで

System.Threading.ThreadPool.QueueUserWorkItem(
    (state) =>
    {
        // 別スレッドでの処理
    }, 123 );

のようにも記述できます。

Timerクラス

System.Threading.Timerクラスは、内部ではThreadPoolのQueueUserWorkItem()でスレッドを実行します。

.NET Frameworkにはこれと同名のTimerクラスが4つあるため、usingディレクティブの宣言に注意します。

  • System.Threading.Timer
  • System.Timers.Timer
  • System.Windows.Forms.Timer
  • System.Web.UI.Timer
public Timer(
    TimerCallback callback,  // 実行するメソッド
    Object state,            // メソッドへ渡すデータ
    int dueTime,             // メソッドが呼び出されるまでの遅延時間 [msec]
    int period               // メソッドが呼び出される周期 [msec]
    )
Timer コンストラクター (TimerCallback, Object, Int32, Int32) (System.Threading) | MSDN
  • dueTimeを0 … タイマーをすぐに開始する
  • dueTimeをTimeout.Infinite … タイマーを開始しない
  • periodをTimeout.Infinite … 周期的に呼び出さない

指定時間に呼び出されるメソッドは引数のcallbackで、TimerCallbackデリゲートの形式で指定します。

delegate void TimerCallback( Object state )
TimerCallback デリゲート (System.Threading) | MSDN

dueTimeperiodは、Change()メソッドで後から変更できます。

public bool Change(
    int dueTime, // 遅延時間 [msec]
    int period   // 呼び出し周期 [msec]
)
Timer.Change メソッド (Int32, Int32) (System.Threading) | MSDN
引数 作用
dueTime タイマーをすぐに再開する
dueTime Timeout.Infinite タイマーを停止する
period Timeout.Infinite 周期的な呼び出しを無効にする

サンプルコード

void Main()
{
    System.Threading.Timer timer
        = new System.Threading.Timer( MethodName, null, 100, 200 );
}

void MethodName( Object state )
{
    // このメソッドが制御を戻すと、スレッドはスレッドプールに戻り次のタスクを待つ
}

これはラムダ式を用いると、次のようにも記述できます。

System.Threading.Timer timer = new System.Threading.Timer(
    (state) =>
    {
        // 別スレッドでの処理
    }, null, 100, 200);

Visual Studioのタイマー

Visual StudioのデザイナにあるタイマーはSystem.Windows.Forms.Timerであり、前述のSystem.Threading.Timerとは異なります。このクラスはすべての処理を1つのスレッドで行うため、複数のタイマを同時に実行できません。


Visual Studioのツールボックス

Threadクラス

Threadクラスでは処理の優先度を変更したり、アプリケーションの終了に伴い途中で終了されてしまうのを防ぐことができます。

既定ではフォアグラウンド スレッドで実行され、アプリケーションを終了してもスレッドは終了しません。

スレッドで実行するメソッドはParameterizedThreadStartデリゲート型ですが、この書式はWaitCallbackデリゲートと同一です。ParameterizedThreadStart デリゲート (System.Threading) | MSDN

public Thread( ParameterizedThreadStart start )
Thread コンストラクター (ParameterizedThreadStart) (System.Threading) | MSDN

プロパティ

プロパティ  
bool IsAlive trueならば、スレッドは起動しており、正常終了も中止もされていない。より詳細な情報はThreadStateで確認できる
ThreadState ThreadState スレッドの状態 (ビットの組み合わせで表現される)。これはデバッグ用であり、スレッドの制御に用いてはならない
bool IsBackground trueならば、バックグラウンド スレッド
bool IsThreadPoolThread trueならば、マネージド スレッドプールに所属
ThreadPriority Priority スケジューリング優先度
Thread CurrentThread 現在実行中のスレッド
int ManagedThreadId プロセス内で一意なスレッドの識別子。これはスレッド ウィンドウのマネージIDと一致する
プロパティ - Thread クラス (System.Threading) | MSDN
ThreadState 列挙型
状態
Aborted  
AbortRequested  
Background バックグラウンド スレッドで実行中
Running  
Stopped  
StopRequested  
Suspended  
SuspendRequested  
Unstarted まだStart()が呼ばれていない
WaitSleepJoin  
ThreadState 列挙型 (System.Threading) | MSDN

メソッド

メソッド  
Start() ThreadStateをRunningに変更する
Abort() ThreadAbortException例外を発生させ、スレッドを終了させる
Interrupt() ThreadStateがWaitSleepJoinであるスレッドを中断させる
Join() スレッドが終了するまで、呼び出し元のスレッドをブロックする
メソッド - Thread クラス (System.Threading) | MSDN
スレッドの起動

スレッドは、Start()メソッドの呼び出しにより開始できます。

public void Start( object parameter )
Thread.Start メソッド (Object) (System.Threading) | MSDN

Start()で開始したスレッドは、終了した後でもStart()で再開させることはできません。その必要があるならば、あらためてThreadのインスタンスを生成し、それをStart()で開始させます。Remarks - Thread.Start Method (System.Threading) | MSDN Remarks - ThreadStateException Class (System.Threading) | MSDN

スレッドの中断
public static void Sleep(
    int millisecondsTimeout // スレッドを中断する時間 [msec]
)
Thread.Sleep メソッド (System.Threading) | MSDN

millisecondsTimeoutの時間、現在のスレッドを一時停止させられます。これは静的メソッドのため、メインスレッドからも直接呼び出せます。

サンプルコード

void Main()
{
    System.Threading.Thread thread = new System.Threading.Thread(MethodName);

    // スレッドを起動する
    thread.Start(123);
}

void MethodName( Object param )
{
    // このメソッドが制御を戻すと、スレッドは破棄される
}

ラムダ式では次のように記述できます。

new System.Threading.Thread( param =>
{
    // 別スレッドでの処理
}).Start(123);;

Taskクラス

System.Threading.Tasks.Task task = System.Threading.Tasks.Task.Run(() =>
{
    // 別スレッドでの処理
});

Taskクラスで作成したスレッドは、既定でバックグラウンド スレッドで実行されます。c# 4.0 - Are Tasks created as background threads? - Stack Overflow

インスタンス化

public Task(
    Action action
)
Task コンストラクター (Action) (System.Threading.Tasks) | MSDN Task コンストラクター (System.Threading.Tasks) | MSDN
Action action = () =>
{
    Console.WriteLine("Task ID:{0}", Task.CurrentId);
};


// インスタンス化し、開始する
Task task1 = Task.Factory.StartNew(action);

// インスタンス化し、開始する (StartNew()の代替)
Task task2 = Task.Run(action);

// コンストラクタでインスタンス化する。そしてStart()で開始する
Task task3 = new Task(action);
task3.Start();

// コンストラクタでインスタンス化する。そしてRunSynchronously()でメインスレッドで同期実行する
Task task4 = new Task(action);
task4.RunSynchronously();
Task instantiation - Task Class (System.Threading.Tasks) | Microsoft Docs

値を返す場合には次のようにします。

Func<int> action = () =>
{
    return 1;
};

Task<int> task1 = Task.Factory.StartNew(action);
int result1 = task1.Result;

Task<int> task2 = Task.Run(action);
int result2 = task2.Result;

Task<int> task3 = new Task<int>(action);
task3.Start();
int result3 = task3.Result;

プロパティ

プロパティ  
AggregateException Exception Taskが早く終了する原因を表すAggregateException
TaskFactory Factory ファクトリ メソッド
int Id Taskに割り当てられた識別子。作成順の番号となるとは限らず、一意であることも保証されない
bool IsCompleted trueならば、Taskが完了している。完了とはRanToCompletion、Faulted、Canceledの3つの状態のいずれか
TaskStatus Status タスクの状態
プロパティ - Task クラス (System.Threading.Tasks) | MSDN

メソッド

メソッド  
ConfigureAwait(Boolean) Taskを待機する方法を構成する
Delay(Int32) 遅延した後に完了するタスクを作成する
Run(Action) 指定の処理をスレッドプールに並ばせ、その処理を表すTaskを返す
Start() タスクを開始し、現在のTaskSchedulerにその実行を予定する
Wait() タスクの実行が完了するまで待機する
WaitAll(Task[]) すべてのタスクの実行が完了するまで待機する
メソッド - Task クラス (System.Threading.Tasks) | MSDN
Run()

.NET Framework 4.5以降は、細かい制御が不要ならばTaskFactory.StartNew()の代わりにこのメソッドを用います。Remarks - TaskFactory.StartNew Method (System.Threading.Tasks) | Microsoft Docs

public static Task Run(
    Action action
)
Task.Run メソッド (Action) (System.Threading.Tasks) | MSDN

指定の処理をスレッドプールに並ばせ、その処理を表すTask<TResult>を受け取れます。

public static Task<TResult> Run<TResult>(
    Func<TResult> function // 非同期に実行する処理
)
Task.Run(TResult) メソッド (Func(TResult)) (System.Threading.Tasks) | MSDN
FromResult()

FromResult()は、結果だけを返すときに用います。

public static Task<TResult> FromResult<TResult>(
    TResult result // 完了したタスクに格納する結果
)
Task.FromResult(TResult) メソッド (TResult) (System.Threading.Tasks) | MSDN
int cache = 0;

public async Task<int> Bar()
{
    int result;
    if (cache != 0)
    {
        result = await Task.FromResult(cache * 2);
    }
    else
    {
        result = await Task.Run(() =>
        {
            cache = 10;
            return cache;
        });
    }
    return result;
}

public async void Foo()
{
    int a1 = await Bar(); // 10が返される
    int a2 = await Bar(); // 20が返される
}
Wait()

タスクが完了するまで、呼び出し元のスレッドをブロックします。

public void Wait()
Task.Wait メソッド (System.Threading.Tasks) | MSDN

これにより呼び出し元のスレッドと同期させられます。

Task task = Task.Run(() =>
{
});

task.Wait(); // 呼び出し元のスレッドをブロックし、タスクの完了まで待機する

// これ以降はタスクの完了後に処理される

スレッドがブロックされることを望まず非同期で処理するならば、awaitで待機させます。c# - await vs Task.Wait - Deadlock? - Stack Overflow

await Task.Run(() =>
{
});
// 呼び出し元のスレッドはブロックされず、呼び出し元へ制御が戻される

// これ以降はタスクの完了後に処理される

非同期メソッドが値を返すならば、Task.Wait()ではなくTask<TResult>.Resultプロパティのgetアクセサにアクセスすることで同期させられます。

Task<int> task = Task.Run(() =>
{
    return 1;
});

int result = task.Result; // 呼び出し元のスレッドをブロックし、タスクの完了まで待機する
ConfigureAwait()

falseを指定すると、Taskの待機後に呼び出し元のスレッドに戻されなくなります。

public ConfiguredTaskAwaitable ConfigureAwait(
    bool continueOnCapturedContext
)
Task.ConfigureAwait メソッド (Boolean) (System.Threading.Tasks) | MSDN
await Task.Run(() =>
{
    // ここは別スレッド
});

// ここでは呼び出し元のスレッドに戻される
await Task.Run(() =>
{
    // ここは別スレッド
}).ConfigureAwait(false);

// 呼び出し元へは戻されず、Task内と同一のスレッド

同期処理

lockステートメント

指定のコードブロックに対して、複数のスレッドが同時にアクセスできないように制限できます。

lock (obj)
    statement
lock ステートメント (C# リファレンス) | MSDN

objは同期が必要なリソースで、ロックのスコープを定義するために使用されます。複数のスレッドが同一のオブジェクトをロックすることによるデッドロックを防ぐため、publicなオブジェクトなどは避けます。statementには複数のスレッドからのアクセスを認められない文を記述し、あるスレッドがこのstatementを抜けるまで、他のスレッドはこの直前で待機させられます。

たとえばあるオブジェクトを初期化する場合を考えます。

private readonly object syncLock = new Object(); // ロック用のオブジェクト


if (target == null)
{
    lock (syncLock)
    {
        // lockの直前に他のスレッドによって書き換えられていないか再確認する
        if (target == null)
        {
            target = new MyClass();
        }
    }
}

lockステートメントはMonitorクラスのEnterとExitを呼ぶショートカットに過ぎず、

lock (x)
{
    // 何らかの処理
}

と記述することは、

System.Object obj = (System.Object)x;
System.Threading.Monitor.Enter(obj);

try
{
    // 何らかの処理
}
finally
{
    System.Threading.Monitor.Exit(obj);
}

とすることと同義です。Monitor - スレッドの同期 (C# および Visual Basic)

Monitor.Enter()

public static void Enter(
    object obj // モニター ロックを取得する対象となるオブジェクト
)
Monitor.Enter メソッド (Object) (System.Threading)

他のスレッドがobjを対象にEnter()を実行すると、そのスレッドがExit()を実行するまで現在のスレッドがブロックされます。

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

ThreadStatic属性

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

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

ThreadLocal<T>クラス

ThreadLocal(T) クラス (System.Threading) | MSDN

Thread.SetData()、Thread.GetData()

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

Threadクラス

Threadクラスで開始されたスレッド内から投げられた例外は、その呼び出し元のスレッドでは捕捉できません。

try
{
    new Thread(() =>
    {
        throw new InvalidOperationException("ERROR");
    }).Start();
}
catch (Exception e)
{
    Console.WriteLine(e.Message); // 捕捉されず、例外によってプロセスが中止させられる
}

しかし当然、スレッド内では捕捉できます。

new Thread(() =>
{
    try
    {
        throw new InvalidOperationException("ERROR");
    }
    catch (Exception e)
    {
        Console.WriteLine(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.WriteLine(e.Message); // 捕捉されず、例外は失われる
}

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

try
{
    Task task = new Task(() =>
    {
        throw new InvalidOperationException("ERROR");
    });

    task.ContinueWith((_task) =>
    {
        Console.WriteLine(_task.Exception.Message); // ここで捕捉される。"1 つ以上のエラーが発生しました。"
    }, TaskContinuationOptions.OnlyOnFaulted);

    task.Start();
}
catch (Exception e)
{
    Console.WriteLine(e.Message); // すでに捕捉されているため、ここへは到達しない
}
public Task ContinueWith(
    Action<Task> continuationAction,            // 実行するアクション
    TaskContinuationOptions continuationOptions // タスクの動作条件
)
Task.ContinueWith メソッド (Action(Task), TaskContinuationOptions) (System.Threading.Tasks) | MSDN

Wait()で待機させた場合にはAggregateException例外を捕捉することで、そこで発生したすべての例外を確認できます。

Task task = Task.Run(() =>
{
    throw new InvalidOperationException("ERROR");
});

try
{
    task.Wait(); // スレッドの終了を待機
}
catch (AggregateException e)
{
    Console.WriteLine(e.Message); // 捕捉される。"1 つ以上のエラーが発生しました。"
    Console.WriteLine(e.InnerExceptions[0].Message); // "ERROR"
}

一方でawaitで待機させた場合には、普通に捕捉できます。

try
{
    await Task.Run(() =>
    {
        throw new InvalidOperationException("ERROR");
    });
}
catch (Exception e)
{
    Console.WriteLine(e.Message); // 捕捉される。"ERROR"
}

スレッドのデバッグ

Visual Studioでは、スレッド ウィンドウで現在アクティブなスレッドを確認できます。ThreadクラスのオブジェクトはNameプロパティからスレッド名を付けることで、そこでの識別が容易になります。またマネージIDは、ManagedThreadIdプロパティの値となっています。

出力ウィンドウで[スレッド終了メッセージ]が出力されるように設定されていると、スレッド終了時に「スレッド 0x**** はコード 0 (0x0) で終了しました。(The thread 0x**** has exited with code 0 (0x0).)」のように表示されます。

MSDN (Microsoft Developer Network) から検索