目次
- はじめに
- 対象読者
- 必要な環境
- 新しいスレッドを作成し、実行する
- フォアグラウンドスレッドとバックグラウンドスレッド
- スレッドが終了するまで待機する
- スレッドの同期
- 競合状態
- Monitor.Enter・Monitor.Exitメソッド
- lock・SyncLockステートメント
- 静的メソッドの同期
- ロック専用のオブジェクト
- MethodImplOptions.Synchronized
- デッドロック
- まとめ
- 参考資料
はじめに
本稿では、.NET Frameworkにおけるマルチスレッドプログラミングについて、次のような方針で解説します。また、本稿は四部作の「パート1」です。
- マルチスレッドプログラミングの基本事項を、全般的に紹介します。ただし、説明は簡潔に要点のみを押さえ、深い部分までは掘り下げません。より詳しい内容を知りたい方は、記事内に示すリンク先や、末尾の「参考資料」を参照してください。
- この記事では、筆者が重要と判断した項目をなるべく前の方で紹介しています。特にパート2までは、筆者が絶対に知っておくべきと判断した話題です。一般的な入門書のように系統立てて構成していないため、多少分かりづらい点があるかもしれません。
- 直感的に理解できるように、各項目のはじめに簡単なサンプルを示しています。そのため、まずはサンプルをダウンロードし、実際の動作を確認してみることをお勧めします。
- サンプルでは、できるだけ記述を簡潔にするために例外処理を省略しています。実際には、例外処理を実装する必要があります。
- 「スレッドとは何か」「どのような時にマルチスレッドとすべきか」という点に関しては、この記事ではほとんど触れません。これらに関しては、下記の補足をご覧ください。
マルチスレッドプログラミングは非常に複雑で難しく、多くの危険を伴います。そのため、マルチスレッドを実装する必然性がないのであれば、手を出すべきではありません。
しかし、それでもなおマルチスレッドプログラミングを習得したいという方々にとって、この記事が微力ながらもお役に立てれば幸いです。
- MSDNライブラリ 『.NET Framework開発者ガイド-スレッドおよびスレッド処理』
- MSDNライブラリ 『プログラミングC#-15.1 スレッドの基本』
- MSDNライブラリ 『.NET Framework開発者ガイド-スレッドおよびスレッド処理』
- MSDNライブラリ 『プログラミングC#-15.5 スレッド操作指針』
- MSDNライブラリ 『Chapter 5-Improving Managed Code Performanceの「Threading Guidelines」~「Consider parallel vs. synchronous tasks.」』
- Jon's Homepage! 『When to use Threads』
対象読者
.NET Frameworkにおけるマルチスレッドプログラミングについて勉強したいという方を対象としています。ただし、ここではプログラミングの基本的な事柄については説明しませんので、不明な点はMSDNライブラリ等で調べてください。
必要な環境
サンプルはVisual Studio .NET 2003で作成し、.NET Framework 1.1で動作確認をしています。
新しいスレッドを作成し、実行する
.NET Frameworkでは複数のスレッドを使用する方法として、主に次の3つが用意されています。
これらの使い分けについては、各見出しで説明することとし、まずはThread
オブジェクトを使った方法から説明します。
サンプル「start_01」は、マルチスレッドを使わない(つまりシングルスレッドの)普通のコンソールアプリケーションです。
Class Class1 'エントリポイント Public Shared Sub Main() Console.WriteLine("スタート") 'DoSomethingメソッドを実行 DoSomething() Console.WriteLine("Enterキーを押してください") Console.ReadLine() End Sub 'メソッド Private Shared Sub DoSomething() '長い時間のかかる処理があるものとする Dim i As Long For i = 0 To 1000000000 Next i '処理が終わったことを知らせる Console.WriteLine("終わりました") End Sub End Class
class Class1 { //エントリポイント public static void Main() { Console.WriteLine("スタート"); //DoSomethingメソッドを実行 DoSomething(); Console.WriteLine("Enterキーを押してください"); Console.ReadLine(); } //メソッド private static void DoSomething() { //長い時間のかかる処理があるものとする for (long i = 0; i < 1000000000; i++); //処理が終わったことを知らせる Console.WriteLine("終わりました"); } }
このアプリケーションでは、当然のことですが、次のような順番で文字列が出力されます。
スタート 終わりました Enterキーを押してください。
サンプル「start_02」は、マルチスレッドを使用しています。果たして、どう変わったでしょうか?
Class Class1 'エントリポイント Public Shared Sub Main() Console.WriteLine("スタート") 'DoSomethingメソッドを別のスレッドで実行する 'Threadオブジェクトを作成する Dim t As New System.Threading.Thread( _ New System.Threading.ThreadStart( _ AddressOf DoSomething)) 'スレッドを開始する t.Start() Console.WriteLine("Enterキーを押してください") Console.ReadLine() End Sub '別スレッドで実行するメソッド Private Shared Sub DoSomething() '長い時間のかかる処理があるものとする Dim i As Long For i = 0 To 1000000000 Next i '処理が終わったことを知らせる Console.WriteLine("終わりました") End Sub End Class
class Class1 { //エントリポイント public static void Main() { Console.WriteLine("スタート"); //DoSomethingメソッドを別のスレッドで実行する //Threadオブジェクトを作成する System.Threading.Thread t = new System.Threading.Thread( new System.Threading.ThreadStart(DoSomething)); //スレッドを開始する t.Start(); Console.WriteLine("Enterキーを押してください"); Console.ReadLine(); } //別スレッドで実行するメソッド private static void DoSomething() { //長い時間のかかる処理があるものとする for (long i = 0; i < 1000000000; i++); //処理が終わったことを知らせる Console.WriteLine("終わりました"); } }
このコードでは、DoSomething
メソッドを新たに作成したスレッドで実行しています。Thread.Start
メソッドを呼び出すことにより、スレッドの実行を開始しています。
このアプリケーションを実行すると、文字列が出力される順番が次のようになります。
スタート Enterキーを押してください 終わりました
つまり、DoSomething
メソッドが開始されてから終わるまでの間に、Main
メソッドでは次の処理(Console.WriteLine
)が行われていることが分かります。
先のシングルスレッドの例と違い、DoSomething
メソッドが別スレッドで開始されたため、Main
メソッドを実行しているスレッド(「メインスレッド」、または「プライマリスレッド」)がDoSomething
の処理でブロックされることなく、2つのスレッドが平行して同時に実行されます。
以上のような新しいスレッドを作成してメソッドを実行する手順をまとめると、次のようになります。
- 引数も戻り値もないメソッドを作成し、この中に別スレッドで実行させる処理を記述する。
- このメソッドを指定して、
ThreadStart
デリゲートオブジェクトを作成する。 ThreadStart
オブジェクトを指定して、Thread
オブジェクトを作成する。Thread
オブジェクトのStart
メソッドを呼び出して、スレッドを開始する。
フォアグラウンドスレッドとバックグラウンドスレッド
上に紹介したマルチスレッドアプリケーション「thread_02」をもう一度実行してみてください。このアプリケーションを実行して、「Enterキーを押してください」と表示された直後に[Enter]キーを押したらどうなるでしょうか? たとえ[Enter]キーを押しても、「終わりました」と表示されるまではアプリケーションが終了しません。つまり、Main
メソッドが終了しても、DoSomething
メソッドが終了するまでアプリケーションは終了しません。
このコードに一行だけコードを加えて、サンプル「background_01」を作成しました。これでどう変わったでしょうか?
Class Class1 'エントリポイント Public Shared Sub Main() Console.WriteLine("スタート") 'DoSomethingメソッドを別のスレッドで実行する 'Threadオブジェクトを作成する Dim t As New System.Threading.Thread( _ New System.Threading.ThreadStart( _ AddressOf DoSomething)) '↓このコードを加えた↓ t.IsBackground = True 'スレッドを開始する t.Start() Console.WriteLine("Enterキーを押してください") Console.ReadLine() End Sub '別スレッドで実行するメソッド Private Shared Sub DoSomething() '長い時間のかかる処理があるものとする Dim i As Long For i = 0 To 1000000000 Next i '処理が終わったことを知らせる Console.WriteLine("終わりました") End Sub End Class
class Class1 { //エントリポイント public static void Main() { Console.WriteLine("スタート"); //DoSomethingメソッドを別のスレッドで実行する //Threadオブジェクトを作成する System.Threading.Thread t = new System.Threading.Thread( new System.Threading.ThreadStart(DoSomething)); //↓このコードを加えた↓ t.IsBackground = true; //スレッドを開始する t.Start(); Console.WriteLine("Enterキーを押してください"); Console.ReadLine(); } //別スレッドで実行するメソッド private static void DoSomething() { //長い時間のかかる処理があるものとする for (long i = 0; i < 1000000000; i++); //処理が終わったことを知らせる Console.WriteLine("終わりました"); } }
このアプリケーションでは、「Enterキーを押してください」と表示された直後に[Enter]キーを押すと、「終わりました」と表示される前であってもすぐにアプリケーションが終了します。つまり、DoSomething
メソッドが終了しなくても、Main
メソッドが終了すればアプリケーションは終了します。
この違いは、フォアグランドスレッドとバックグラウンドスレッドの違いです。はじめの例(「thread_02」)では、フォアグランドスレッドでDoSomething
メソッドを実行しました。これに対して、ここで示した例(「background_01」)では、Thread.IsBackground
プロパティをtrue
にしてバックグラウンドスレッドでDoSomething
メソッドを実行しました。
プロセス内のすべてのフォアグラウンドスレッドが終了した時に、そのプロセスは終了し、同時にすべてのバックグランドスレッドは強制的に終了させられます。逆に言えば、すべてのフォアグラウンドスレッドが終了しなければプロセスは終了しませんが、バックグランドスレッドが終了しなくてもプロセスは終了します。
IsBackground
プロパティのデフォルト値がfalse
のため、何も指定しないとフォアグラウンドスレッドになってしまうことに注意してください。このことを知らないと、「メインスレッドが終了したのになぜかアプリケーションが終了しない」と頭を悩ませることになります。
スレッドが終了するまで待機する
上のコードにさらに1行加えて、サンプル「join_01」を作成しました。今度はどうなるでしょうか?
Class Class1 'エントリポイント Public Shared Sub Main() Console.WriteLine("スタート") 'DoSomethingメソッドを別のスレッドで実行する 'Threadオブジェクトを作成する Dim t As New System.Threading.Thread( _ New System.Threading.ThreadStart( _ AddressOf DoSomething)) t.IsBackground = True 'スレッドを開始する t.Start() '↓この行を追加↓ t.Join() Console.WriteLine("Enterキーを押してください") Console.ReadLine() End Sub '別スレッドで実行するメソッド Private Shared Sub DoSomething() '長い時間のかかる処理があるものとする Dim i As Long For i = 0 To 1000000000 Next i '処理が終わったことを知らせる Console.WriteLine("終わりました") End Sub End Class
class Class1 { //エントリポイント public static void Main() { Console.WriteLine("スタート"); //DoSomethingメソッドを別のスレッドで実行する //Threadオブジェクトを作成する System.Threading.Thread t = new System.Threading.Thread( new System.Threading.ThreadStart(DoSomething)); t.IsBackground = true; //スレッドを開始する t.Start(); //↓この行を追加↓ t.Join(); Console.WriteLine("Enterキーを押してください"); Console.ReadLine(); } //別スレッドで実行するメソッド private static void DoSomething() { //長い時間のかかる処理があるものとする for (long i = 0; i < 1000000000; i++); //処理が終わったことを知らせる Console.WriteLine("終わりました"); } }
t.Join
を追加する前は、
スタート Enterキーを押してください 終わりました
という順番で文字列が出力されていましたが、t.Join
を追加した後は、
スタート 終わりました Enterキーを押してください
という順番になります。
Thread
クラスのJoin
メソッドは、そのスレッドが終了するまで現在のスレッドをブロックします。つまり上記の例では、DoSomething
メソッドを実行しているスレッドが終了するまで、メインスレッドはJoin
メソッドにより待機させられます。その結果、DoSomething
メソッドが終了した後に、「Console.WriteLine("Enterキーを押してください")
」が実行されます。
Join
メソッドは、別のスレッドで行っている処理が確実に終了しなければ次に進めない場合に使用します。つまり、スレッドの同期に使用します。スレッドの同期処理については、次項の「スレッドの同期」で説明します。