スレッド (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 … タイマーを開始しない。ただしChange()を呼ぶことで、後から開始できる
  • 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 0 または 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);

タイマーの破棄

タイマーの破棄を確実にするには、引数でWaitHandleを受け取るDispose()を呼びます。multithreading - c# System.Threading.Timer wait for dispose - Stack Overflow

public bool Dispose (
    System.Threading.WaitHandle notifyObject
    );
Dispose(WaitHandle) - Timer.Dispose Method (System.Threading) | Microsoft Docs
using (System.Threading.WaitHandle waitHandle = new System.Threading.ManualResetEvent(false))
{
    if (timer.Dispose(waitHandle))
    {
        // まだ破棄されていないならば、破棄されるのを待つ

        const int millisecondsTimeout = 1000;
        if (!waitHandle.WaitOne(millisecondsTimeout))
        {
            // 指定時間内に破棄されなかった
        }
    }
}

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と一致する
string Name スレッドの名前
プロパティ - 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

コンストラクタを呼ぶ以外に、TaskFactory.StartNew()やTask.Run()で生成する方法もあります。

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に割り当てられた識別子。作成順の番号となるとは限らず、一意であることも保証されない
TaskStatus Status タスクの状態
bool IsCompleted trueならば、Taskが完了している。完了とはRanToCompletion、Faulted、Canceledの3つの状態のいずれか
bool IsCanceled trueならば、Taskがキャンセルされたことによって完了している。これは次のいずれかの状況を表します。
  • Taskが開始される前にCancellationTokenがマークされている。
  • Taskに関連づけられたCancellationTokenからOperationCanceledExceptionが投げられたことで、キャンセルが要求されている。
Remarks - Task.IsCanceled Property (System.Threading.Tasks) | Microsoft Docs
プロパティ - 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> Method1()
{
    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 Method2()
{
    int a1 = await Method1(); // 10が返される
    int a2 = await Method1(); // 20が返される
}
Wait()

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

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

これにより呼び出し元のスレッドと同期させられます。なお完了前にタスクがキャンセルされたときには、AggregateException例外が投げられます。

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; // 呼び出し元のスレッドをブロックし、タスクの完了まで待機する
Wait(CancellationToken)
public void Wait (System.Threading.CancellationToken cancellationToken);
Wait(CancellationToken) - Task.Wait Method (System.Threading.Tasks) | Microsoft Docs

引数にCancellationTokenをとるWait()では、タスクが完了するか、キャンセル トークン (cancellation token) がキャンセルされるまで待機します。キャンセル トークンがキャンセルされたときには、このメソッドはOperationCanceledExceptionを投げます。

ConfigureAwait()

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

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

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

// 呼び出し元へは戻されず、Task内と同一のスレッド
Dispose()
public void Dispose ();
Task.Dispose Method (System.Threading.Tasks) | Microsoft Docs

Dispose()を呼び出せるのは、TaskがRanToCompletion、Faulted、またはCanceledの状態であるときのみです。

基本的にDispose()を呼び出す必要はありません。Do I need to dispose of Tasks? | Parallel Programming with .NET

タスクのキャンセル

キャンセルできたことをTask.IsCanceledで検証したいならば、それで用いるCancellationTokenをコンストラクタで渡します。c# - Cancellation token in Task constructor: why? - Stack Overflow

public Task (
    Action action,
    System.Threading.CancellationToken cancellationToken
    );
Task(Action, CancellationToken) - Task Constructor (System.Threading.Tasks) | Microsoft Docs
CancellationTokenSource tokenSource = new CancellationTokenSource();
CancellationToken token = tokenSource.Token;

Task task = Task.Run(() =>
{
    while (true)
    {
        if (token.IsCancellationRequested)
        {
            // キャンセルを要求されたならば、処理を抜ける
            break;
        }
    }
}, token);


// キャンセルを要求する
tokenSource.Cancel();

CancellationToken.ThrowIfCancellationRequested()を呼び出す方法もありますが、これは

if (token.IsCancellationRequested)
    throw new OperationCanceledException(token);

とすることと同義です。CancellationToken.ThrowIfCancellationRequested Method (System.Threading) | Microsoft Docs

同期処理

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

Win32のクリティカル セクション同等の機能が提供されます。このクラスの機能は、lockによって簡潔に実装できます。

Monitor.Enter()

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

他のスレッドがobjを対象にEnter()を実行していると、そのスレッドがobjを解放するまで現在のスレッドはブロックされます。ロックを取得できるか事前に確認するには、TryEnter()を用います。

public static bool TryEnter (object obj);
Monitor.TryEnter Method (System.Threading) | Microsoft Docs

取得したロックは、Exit()で解放します。

Monitor.Exit

public static void Exit (object obj);
Monitor.Exit(Object) Method (System.Threading) | Microsoft Docs

参考

参考書

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

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プロパティからスレッド名を付けておくと、そこでの識別が容易になります。

void Callback(object state)
{
    Thread thread = Thread.CurrentThread;

    Console.WriteLine(thread.ManagedThreadId); // マネージ ID (マネージ スレッドの識別番号)
    Console.WriteLine(thread.Name);            // 名前 (スレッド名)
}

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

MSDN (Microsoft Developer Network) から検索