目次
- はじめに
- 対象読者
- 必要な環境
- スレッドの優先順位
- スレッドを強制終了する
- スレッドを一時停止する
- スレッドの状態
- 待機中のスレッドを中断する
- スレッドローカルストレージ
- シングルトンとダブルチェックロッキング
- 最後に
- 参考資料
はじめに
本稿は、.NET Frameworkにおけるマルチスレッドプログラミングについて、四部作で解説してきた記事の最終回(パート4)です。パート4では、スレッドの優先順位を変更したり、スレッドを強制終了・一時停止したりする方法について解説します。
対象読者
この記事はパート1、パート2、パート3の続きですので、パート1からお読みください。
必要な環境
サンプルはVisual Studio .NET 2003で作成し、.NET Framework 1.1で動作確認をしています。
スレッドの優先順位
サンプル「priority_01」は、現在のスレッドの名前を1000回出力するメソッドを、2つのスレッドで実行するだけのものです。
Class Class1 'エントリポイント Shared Sub Main() '1つ目のスレッドを作成し、開始する Dim t1 As New System.Threading.Thread( _ New System.Threading.ThreadStart(AddressOf WriteName)) t1.Name = "1" t1.Start() 'ちょっと待機 System.Threading.Thread.Sleep(10) '2つ目のスレッドを作成し、開始する Dim t2 As New System.Threading.Thread( _ New System.Threading.ThreadStart(AddressOf WriteName)) t2.Name = "2" t2.Start() Console.ReadLine() End Sub '別スレッドで呼び出すメソッド Private Shared Sub WriteName() Dim i As Integer For i = 0 To 999 'スレッドの名前を出力する Console.Write(System.Threading.Thread.CurrentThread.Name) Next i End Sub End Class
class Class1 { //エントリポイント static void Main() { //1つ目のスレッドを作成し、開始する System.Threading.Thread t1 = new System.Threading.Thread( new System.Threading.ThreadStart(WriteName)); t1.Name = "1"; t1.Start(); //ちょっと待機 System.Threading.Thread.Sleep(10); //2つ目のスレッドを作成し、開始する System.Threading.Thread t2 = new System.Threading.Thread( new System.Threading.ThreadStart(WriteName)); t2.Name = "2"; t2.Start(); Console.ReadLine(); } //別スレッドで呼び出すメソッド private static void WriteName() { for (int i = 0; i < 1000; i++) { //スレッドの名前を出力する Console.Write( System.Threading.Thread.CurrentThread.Name); } } }
このコードを実行すると、下図のように、通常は「1」と「2」が交互に出力されると思います。
次に、このコードに一行追加してみましょう。
Class Class1 'エントリポイント Shared Sub Main() '1つ目のスレッドを作成し、開始する Dim t1 As New System.Threading.Thread( _ New System.Threading.ThreadStart(AddressOf WriteName)) t1.Name = "1" t1.Start() 'ちょっと待機 System.Threading.Thread.Sleep(10) '2つ目のスレッドを作成し、開始する Dim t2 As New System.Threading.Thread( _ New System.Threading.ThreadStart(AddressOf WriteName)) t2.Name = "2" '↓この行を追加↓ t2.Priority = System.Threading.ThreadPriority.Highest t2.Start() Console.ReadLine() End Sub '別スレッドで呼び出すメソッド Private Shared Sub WriteName() Dim i As Integer For i = 0 To 999 'スレッドの名前を出力する Console.Write(System.Threading.Thread.CurrentThread.Name) Next i End Sub End Class
class Class1 { //エントリポイント static void Main() { //1つ目のスレッドを作成し、開始する System.Threading.Thread t1 = new System.Threading.Thread( new System.Threading.ThreadStart(WriteName)); t1.Name = "1"; t1.Start(); //ちょっと待機 System.Threading.Thread.Sleep(10); //2つ目のスレッドを作成し、開始する System.Threading.Thread t2 = new System.Threading.Thread( new System.Threading.ThreadStart(WriteName)); t2.Name = "2"; //↓この行を追加↓ t2.Priority = System.Threading.ThreadPriority.Highest; t2.Start(); Console.ReadLine(); } //別スレッドで呼び出すメソッド private static void WriteName() { for (int i = 0; i < 1000; i++) { //スレッドの名前を出力する Console.Write( System.Threading.Thread.CurrentThread.Name); } } }
このコードを実行すると、例えば下図のように、「1」が幾つか出力された後に「2」が連続して1000回出力され、その後残りの「1」が出力されます。
このコードでは、Thread
オブジェクトt2
だけPriority
プロパティを変更しました。Thread
クラスのPriority
プロパティは、スレッドの優先順位を取得、設定するプロパティです。優先順位はThreadPriority
列挙体で指定し、優勢順位が高い方から、Highest
, AboveNormal
, Normal
, BelowNormal
, Lowest
の5段階があります。Priority
プロパティのデフォルトはNormal
ですので、t2
に設定したHighest
はt1
のNormal
よりも優先順位が高くなります。
このようなスレッドの優先順位を利用することにより、例えば、長く時間のかかる処理の間、ユーザーからの入力を待っているような場合に、ユーザーからの入力待ちのためのスレッドの優先順位を高くして、その反応を良くするといったことができます。
なお、このスレッドの優先順位というのは、OSによるスレッドのスケジューリングの優先順位のことです。よってその動作はOSによるということになりそうですが、一般的には優先順位の高いスレッドの処理に、プロセッサがより多くの時間を割く(より多くのプロセッサタイムスライスを割り当てる)ことになります。その結果、優先順位の低いスレッドはより高いスレッドがすべて終了する(あるいは待機状態になる)まで実行されないということにもなります。詳しくは「スレッドのスケジューリング」をご覧ください。
スレッドを強制終了する
サンプル「abort_01」は、長い時間のかかるDoSomething
メソッドを別スレッドで実行しますが、[Enter]キーを押すことにより、強制終了することができます。
Class Class1 'エントリポイント Public Shared Sub Main() 'Threadオブジェクトを作成する Dim t As New System.Threading.Thread( _ New System.Threading.ThreadStart(AddressOf DoSomething)) 'スレッドを開始する t.Start() Console.ReadLine() 'スレッドを中断する t.Abort() 'スレッドが終了するまで待機する t.Join() Console.WriteLine("メインスレッドが終了") Console.ReadLine() End Sub '別スレッドで実行するメソッド Private Shared Sub DoSomething() Try 'ループする Dim i As Integer For i = 0 To 999999 Console.Write(".") Next i Catch e As System.Threading.ThreadAbortException 'Abortが呼び出されたとき Console.WriteLine("Abortが呼び出されました") End Try Console.WriteLine("DoSomethingが終了") End Sub End Class
class Class1 { //エントリポイント public static void Main() { //Threadオブジェクトを作成する System.Threading.Thread t = new System.Threading.Thread( new System.Threading.ThreadStart(DoSomething)); //スレッドを開始する t.Start(); Console.ReadLine(); //スレッドを中断する t.Abort(); //スレッドが終了するまで待機する t.Join(); Console.WriteLine("メインスレッドが終了"); Console.ReadLine(); } //別スレッドで実行するメソッド private static void DoSomething() { try { //ループする for (int i = 0; i < 1000000; i++) Console.Write("."); } catch (System.Threading.ThreadAbortException) { //Abortが呼び出されたとき Console.WriteLine("Abortが呼び出されました"); } Console.WriteLine("DoSomethingが終了"); } }
このアプリケーションを実行すると、「.」が次々と表示されます。この時[Enter]キーを押すと、しばらくしてから「Abortが呼び出されました」と表示され、その後「メインスレッドが終了」と表示されます。
このようにAbort
メソッドを呼び出すことにより、スレッドを中止することができます。ただしAbort
メソッドを呼び出してもすぐにスレッドが中止するわけではありませんので、ここではスレッドが間違いなく終了するまでJoin
メソッドで待機しています。また、Abort
メソッドが呼び出されたスレッドでは、ThreadAbortException
がスローされます。
Abort
メソッドを呼び出すと、スレッドの処理がセーフポイントに達した時に中止されます。セーフポイントに関しては「Thread.Suspend、ガベージ コレクション、およびセーフ ポイント」をご覧ください。 Abort
メソッドが呼び出されたスレッドでスレッドの中止要求をキャンセルするには、Thread.ResetAbort
メソッドを使用します。サンプル「abort_02」では、「abort_01」のDoSomething
メソッドのcatch
内に、ResetAbort
メソッドを呼び出すコードを追加しています。
Class Class1 'エントリポイント Public Shared Sub Main() 'Threadオブジェクトを作成する Dim t As New System.Threading.Thread( _ New System.Threading.ThreadStart(AddressOf DoSomething)) 'スレッドを開始する t.Start() Console.ReadLine() 'スレッドを中断する t.Abort() 'スレッドが終了するまで待機する t.Join() Console.WriteLine("メインスレッドが終了") Console.ReadLine() End Sub '別スレッドで実行するメソッド Private Shared Sub DoSomething() Try 'ループする Dim i As Integer For i = 0 To 999999 Console.Write(".") Next i Catch 'Abortが呼び出されたとき Console.WriteLine("Abortが呼び出されました") 'Abortをキャンセルする System.Threading.Thread.ResetAbort() End Try Console.WriteLine("DoSomethingが終了") End Sub End Class
class Class1 { //エントリポイント public static void Main() { //Threadオブジェクトを作成する System.Threading.Thread t = new System.Threading.Thread( new System.Threading.ThreadStart(DoSomething)); //スレッドを開始する t.Start(); Console.ReadLine(); //スレッドを中断する t.Abort(); //スレッドが終了するまで待機する t.Join(); Console.WriteLine("メインスレッドが終了"); Console.ReadLine(); } //別スレッドで実行するメソッド private static void DoSomething() { try { //ループする for (int i = 0; i < 1000000; i++) Console.Write("."); } catch (System.Threading.ThreadAbortException) { //Abortが呼び出されたとき Console.WriteLine("Abortが呼び出されました"); //Abortをキャンセルする System.Threading.Thread.ResetAbort(); } Console.WriteLine("DoSomethingが終了"); } }
このアプリケーションでは、DoSomething
メソッド実行中に[Enter]キーを押すと、「Abortが呼び出されました」と表示された後、「DoSomethingが終了」と表示され、最後に「メインスレッドが終了」と表示されます。つまり、ResetAbort
メソッドを呼び出さなかったときと比べ、「Console.WriteLine("DoSomethingが終了")
」が実行されるようになります。
MSDNライブラリにもあるように、このAbort
メソッドは使用すべきではありません。Abort
はどこで中断するか予測が付かないため、finally
ブロックの実行中や、静的コンストラクタでスレッドが中断される恐れがあります。
ユーザーがキャンセルボタンをクリックした場合に、このAbort
を使用して処理を中断したくなるかもしれませんが、このような場合でもAbort
を使わずに、volatile修飾子を利用したサンプル「volatile_03」で示したような方法で中断するようにしてください。
スレッドを一時停止する
別のスレッドを一時停止して再開する方法として、Suspend
メソッドとResume
メソッドが用意されています。サンプル「suspend_01」では、その使用例を示しています。
Class Class1 'エントリポイント Public Shared Sub Main() 'WriteNumbersメソッドを別スレッドで実行する 'Threadオブジェクトを作成する Dim t As New System.Threading.Thread( _ New System.Threading.ThreadStart(AddressOf WriteNumbers)) 'スレッドを開始する t.Start() 'しばらく待機する System.Threading.Thread.Sleep(100) Console.WriteLine() Console.WriteLine("Suspendメソッドを呼び出す") 'スレッドを中断する t.Suspend() '再びしばらく待機する System.Threading.Thread.Sleep(5000) '中断したスレッドを再開する t.Resume() Console.WriteLine() Console.WriteLine("Resumeメソッドを呼び出す") 'みたびしばらく待機する System.Threading.Thread.Sleep(100) Console.WriteLine() Console.WriteLine("Abortメソッドを呼び出す") 'スレッドを終了する t.Abort() Console.ReadLine() End Sub '別スレッドで実行するメソッド Private Shared Sub WriteNumbers() '無限ループ While True Dim i As Integer For i = 0 To 9 Console.Write(i) Next i End While End Sub End Class
class Class1 { //エントリポイント public static void Main() { //WriteNumbersメソッドを別スレッドで実行する //Threadオブジェクトを作成する System.Threading.Thread t = new System.Threading.Thread( new System.Threading.ThreadStart(WriteNumbers)); //スレッドを開始する t.Start(); //しばらく待機する System.Threading.Thread.Sleep(100); Console.WriteLine(); Console.WriteLine("Suspendメソッドを呼び出す"); //スレッドを中断する t.Suspend(); //再びしばらく待機する System.Threading.Thread.Sleep(5000); //中断したスレッドを再開する t.Resume(); Console.WriteLine(); Console.WriteLine("Resumeメソッドを呼び出す"); //みたびしばらく待機する System.Threading.Thread.Sleep(100); Console.WriteLine(); Console.WriteLine("Abortメソッドを呼び出す"); //スレッドを終了する t.Abort(); Console.ReadLine(); } //別スレッドで実行するメソッド private static void WriteNumbers() { //無限ループ for (;;) for (int i = 0; i < 10; i++) Console.Write(i); } }
このプログラムを実行すると、次のようになります。
- 数字を幾つか表示
- 「Suspendメソッドを呼び出す」と表示
- 数字を幾つか表示
- 「Resumeメソッドを呼び出す」と表示
- 数字を幾つか表示
- 5秒間停止
- 数字を幾つか表示
- 「Abortメソッドを呼び出す」と表示
Thread
クラスのSuspend
メソッドでスレッドが一時停止し、Resume
メソッドで再開します。Suspend
メソッドは、Abort
メソッドと同様、呼び出してすぐにスレッドが一時停止するわけではなく、セーフポイントまで停止しません。
基本的にSuspend
メソッドは使用すべきではありません。スレッドを同期させたいならばlock
などを使うべきですし、別のスレッドを優先的に処理されるためにあるスレッドを一時停止させたいということであれば、ThreadPriority
プロパティでスレッドの優先順位を指定すべきです。
スレッドの状態
現在のスレッドの状態は、Thread.ThreadState
プロパティで分かります。ThreadState
プロパティが取りうる値とその意味については「ThreadState
」列挙体などを参照してもらうこととし、ここでは具体的なサンプルを示します。サンプル「state_01」を実行させれば、どのような時にThreadState
プロパティがどのような値となるかが分かります。
Class Class1 'エントリポイント Public Shared Sub Main() 'Threadオブジェクトを作成する Dim t As New System.Threading.Thread( _ New System.Threading.ThreadStart(AddressOf DoSomething)) 'スレッドの状態を表示 Console.WriteLine("ThreadState:{0}", t.ThreadState.ToString()) 'スレッドを開始する Console.WriteLine("Startを呼び出す") t.Start() Console.WriteLine("ThreadState:{0}", t.ThreadState.ToString()) System.Threading.Thread.Sleep(1000) Console.WriteLine("ThreadState:{0}", t.ThreadState.ToString()) 'ロックしてスレッドをブロックする Console.WriteLine("ロックしてスレッドをブロックする") System.Threading.Monitor.Enter(GetType(Class1)) Console.WriteLine("ThreadState:{0}", t.ThreadState.ToString()) System.Threading.Thread.Sleep(1000) Console.WriteLine("ThreadState:{0}", t.ThreadState.ToString()) System.Threading.Monitor.Exit(GetType(Class1)) 'スレッドを中断 Console.WriteLine("Suspendを呼び出す") t.Suspend() Console.WriteLine("ThreadState:{0}", t.ThreadState.ToString()) System.Threading.Thread.Sleep(1000) Console.WriteLine("ThreadState:{0}", t.ThreadState.ToString()) 'スレッドの再開 Console.WriteLine("Resumeを呼び出す") t.Resume() Console.WriteLine("ThreadState:{0}", t.ThreadState.ToString()) System.Threading.Thread.Sleep(1000) Console.WriteLine("ThreadState:{0}", t.ThreadState.ToString()) 'スレッドの停止 Console.WriteLine("Abortを呼び出す") t.Abort() Console.WriteLine("ThreadState:{0}", t.ThreadState.ToString()) 'スレッドが終了するまで待つ Console.WriteLine("Joinを呼び出す") t.Join() Console.WriteLine("ThreadState:{0}", t.ThreadState.ToString()) Console.ReadLine() End Sub '別スレッドで実行するメソッド Private Shared Sub DoSomething() '無限ループ While True System.Threading.Monitor.Enter(GetType(Class1)) System.Threading.Monitor.Exit(GetType(Class1)) End While End Sub End Class
class Class1 { //エントリポイント public static void Main() { //Threadオブジェクトを作成する System.Threading.Thread t = new System.Threading.Thread( new System.Threading.ThreadStart(DoSomething)); //スレッドの状態を表示 Console.WriteLine ("ThreadState:{0}", t.ThreadState.ToString()); //スレッドを開始する Console.WriteLine("Startを呼び出す"); t.Start(); Console.WriteLine ("ThreadState:{0}", t.ThreadState.ToString()); System.Threading.Thread.Sleep(1000); Console.WriteLine ("ThreadState:{0}", t.ThreadState.ToString()); //ロックしてスレッドをブロックする Console.WriteLine("ロックしてスレッドをブロックする"); System.Threading.Monitor.Enter(typeof(Class1)); Console.WriteLine ("ThreadState:{0}", t.ThreadState.ToString()); System.Threading.Thread.Sleep(1000); Console.WriteLine ("ThreadState:{0}", t.ThreadState.ToString()); System.Threading.Monitor.Exit(typeof(Class1)); //スレッドを中断 Console.WriteLine("Suspendを呼び出す"); t.Suspend(); Console.WriteLine ("ThreadState:{0}", t.ThreadState.ToString()); System.Threading.Thread.Sleep(1000); Console.WriteLine ("ThreadState:{0}", t.ThreadState.ToString()); //スレッドの再開 Console.WriteLine("Resumeを呼び出す"); t.Resume(); Console.WriteLine ("ThreadState:{0}", t.ThreadState.ToString()); System.Threading.Thread.Sleep(1000); Console.WriteLine ("ThreadState:{0}", t.ThreadState.ToString()); //スレッドの停止 Console.WriteLine("Abortを呼び出す"); t.Abort(); Console.WriteLine ("ThreadState:{0}", t.ThreadState.ToString()); //スレッドが終了するまで待つ Console.WriteLine("Joinを呼び出す"); t.Join(); Console.WriteLine ("ThreadState:{0}", t.ThreadState.ToString()); Console.ReadLine(); } //別スレッドで実行するメソッド private static void DoSomething() { //無限ループ for (;;) { System.Threading.Monitor.Enter(typeof(Class1)); System.Threading.Monitor.Exit(typeof(Class1)); } } }
このプログラムを実行すると、例えば次のように出力されます。
ThreadState:Unstarted Startを呼び出す ThreadState:Unstarted ThreadState:Running ロックしてスレッドをブロックする ThreadState:Running ThreadState:WaitSleepJoin Suspendを呼び出す ThreadState:SuspendRequested ThreadState:Suspended Resumeを呼び出す ThreadState:Running ThreadState:Running Abortを呼び出す ThreadState:AbortRequested Joinを呼び出す ThreadState:Stopped
ThreadState
プロパティでスレッドの状態を調べるには、ビットマスクを使用します。例えば、スレッドが実行中か調べるには、次のようにします。
if ((t.ThreadState & (System.Threading.ThreadState.Unstarted | System.Threading.ThreadState.Stopped)) == 0) { Console.WriteLine("スレッドは実行中です"); }
また、スレッドが実行中かどうか調べるだけであれば、IsAlive
プロパティを使うこともできます。スレッドの状態がUnstarted
かStopped
であればIsAlive
がfalse
となります。
待機中のスレッドを中断する
Thread.Interrupt
メソッドは、待機中のスレッドを中断させます。サンプル「interrupt_01」では、Interrupt
メソッドを使用した例を示します。
Class Class1 'エントリポイント Public Shared Sub Main() 'Threadオブジェクトを作成する Dim t As New System.Threading.Thread( _ New System.Threading.ThreadStart(AddressOf DoSomething)) 'スレッドを開始する t.Start() Console.WriteLine("Enterキーを押してください") Console.ReadLine() '待機状態を中断する Console.WriteLine("Interruptを呼び出す") t.Interrupt() Console.ReadLine() End Sub '別スレッドで実行するメソッド Private Shared Sub DoSomething() Try 'ずっと待機状態にする System.Threading.Thread.Sleep _ (System.Threading.Timeout.Infinite) Catch e As System.Threading.ThreadInterruptedException Console.WriteLine("ThreadInterruptedExceptionがスロー") End Try Console.WriteLine("別スレッドが終了") End Sub End Class
class Class1 { //エントリポイント public static void Main() { //Threadオブジェクトを作成する System.Threading.Thread t = new System.Threading.Thread( new System.Threading.ThreadStart(DoSomething)); //スレッドを開始する t.Start(); Console.WriteLine("Enterキーを押してください"); Console.ReadLine(); //待機状態を中断する Console.WriteLine("Interruptを呼び出す"); t.Interrupt(); Console.ReadLine(); } //別スレッドで実行するメソッド private static void DoSomething() { try { //ずっと待機状態にする System.Threading.Thread.Sleep (System.Threading.Timeout.Infinite); } catch (System.Threading.ThreadInterruptedException) { Console.WriteLine("ThreadInterruptedExceptionがスロー"); } Console.WriteLine("別スレッドが終了"); } }
DoSomething
メソッドを実行する別スレッドは、Thread.Sleep
により無限にブロックされます。しかし、ブロックされているスレッドのInterrupt
メソッドを呼び出すことによって、スレッドを中断します。この時、例外ThreadInterruptedException
がスローされます。
Interrupt
メソッドで中断するスレッドは、ThreadState
プロパティがWaitSleepJoin
の状態のスレッドです。スレッドがWaitSleepJoin
となるのは、Thread.Sleep
メソッドを呼び出したときの他に、Thread.Join
メソッドや、Monitor.Enter
・Wait
メソッドでブロックされているときなどです。なお、WaitSleepJoin
となっていないスレッドでInterrupt
を呼び出した場合は、次にWaitSleepJoin
となったときに中断されます。
このInterrupt
メソッドも基本的には使用すべきではありません。