目次
- はじめに
- 対象読者
- 必要な環境
- Monitor.Wait・Pluseメソッド
- ReaderWriterLockクラス
- Mutexクラス
- スレッドセーフなコレクション
- スレッドタイマ
- ThreadPool.RegisterWaitForSingleObjectメソッド
- まとめ
- 参考資料
はじめに
パート1、パート2では、筆者が.NET Frameworkにおけるマルチスレッドプログラミングで、必ず知っておくべきと判断した事柄を紹介しました。パート3では、その他に知っておくと便利な、Mutexによるプロセス間の同期や、スレッドタイマの利用方法などについて解説します。
対象読者
この記事はパート1、パート2の続きですので、パート1からお読みください。
必要な環境
サンプルはVisual Studio .NET 2003で作成し、.NET Framework 1.1で動作確認をしています。
Monitor.Wait・Pluseメソッド
Monitor
クラスのWait
、Pulse
、PulseAll
メソッドは、Monitor.Enter
・Exit
で同期されたコードのブロック内で使用します。あるスレッドがWait
を呼び出すとオブジェクトのロックが解放され、他のスレッドがPulse
またはPulseAll
を呼び出し、再びロックが取得できるまでブロックされます。
何はともあれ、具体的なサンプルを見てみましょう。サンプル「monitor_01」を見れば、Monitor.Wait
とPulse
がどのような働きをするか分かります。
Class Class1 Private Shared syncObject As New Object 'エントリポイント Public Shared Sub Main() '2つのスレッドを作成し、開始する Dim t1 As New System.Threading.Thread( _ New System.Threading.ThreadStart(AddressOf A)) t1.Name = "A" Dim t2 As New System.Threading.Thread( _ New System.Threading.ThreadStart(AddressOf B)) t2.Name = "B" t1.Start() t2.Start() Console.ReadLine() End Sub Public Shared Sub A() Console.WriteLine("{0}:スレッド開始", _ System.Threading.Thread.CurrentThread.Name) SyncLock syncObject Console.WriteLine("{0}:lock内に進入", _ System.Threading.Thread.CurrentThread.Name) System.Threading.Thread.Sleep(2000) Console.WriteLine("{0}:Wait開始", _ System.Threading.Thread.CurrentThread.Name) System.Threading.Monitor.Wait(syncObject) Console.WriteLine("{0}:Wait終了", _ System.Threading.Thread.CurrentThread.Name) Console.WriteLine("{0}:lockから抜ける", _ System.Threading.Thread.CurrentThread.Name) End SyncLock Console.WriteLine("{0}:スレッド終了", _ System.Threading.Thread.CurrentThread.Name) End Sub Public Shared Sub B() Console.WriteLine("{0}:スレッド開始", _ System.Threading.Thread.CurrentThread.Name) System.Threading.Thread.Sleep(1000) SyncLock syncObject Console.WriteLine("{0}:lock内に進入", _ System.Threading.Thread.CurrentThread.Name) Console.WriteLine("{0}:Pulse開始", _ System.Threading.Thread.CurrentThread.Name) System.Threading.Monitor.Pulse(syncObject) System.Threading.Thread.Sleep(1000) Console.WriteLine("{0}:lockから抜ける", _ System.Threading.Thread.CurrentThread.Name) End SyncLock Console.WriteLine("{0}:スレッド終了", _ System.Threading.Thread.CurrentThread.Name) End Sub End Class
class Class1 { private static readonly object syncObject = new object(); //エントリポイント public static void Main() { //2つのスレッドを作成し、開始する System.Threading.Thread t1 = new System.Threading.Thread( new System.Threading.ThreadStart(A)); t1.Name = "A"; System.Threading.Thread t2 = new System.Threading.Thread( new System.Threading.ThreadStart(B)); t2.Name = "B"; t1.Start(); t2.Start(); Console.ReadLine(); } public static void A() { Console.WriteLine("{0}:スレッド開始", System.Threading.Thread.CurrentThread.Name); lock(syncObject) { Console.WriteLine("{0}:lock内に進入", System.Threading.Thread.CurrentThread.Name); System.Threading.Thread.Sleep(2000); Console.WriteLine("{0}:Wait開始", System.Threading.Thread.CurrentThread.Name); System.Threading.Monitor.Wait(syncObject); Console.WriteLine("{0}:Wait終了", System.Threading.Thread.CurrentThread.Name); Console.WriteLine("{0}:lockから抜ける", System.Threading.Thread.CurrentThread.Name); } Console.WriteLine("{0}:スレッド終了", System.Threading.Thread.CurrentThread.Name); } public static void B() { Console.WriteLine("{0}:スレッド開始", System.Threading.Thread.CurrentThread.Name); System.Threading.Thread.Sleep(1000); lock(syncObject) { Console.WriteLine("{0}:lock内に進入", System.Threading.Thread.CurrentThread.Name); Console.WriteLine("{0}:Pulse開始", System.Threading.Thread.CurrentThread.Name); System.Threading.Monitor.Pulse(syncObject); System.Threading.Thread.Sleep(1000); Console.WriteLine("{0}:lockから抜ける", System.Threading.Thread.CurrentThread.Name); } Console.WriteLine("{0}:スレッド終了", System.Threading.Thread.CurrentThread.Name); } }
このアプリケーションを実行すると、例えば次のように出力されます。
A:スレッド開始 A:lock内に進入 B:スレッド開始 A:Wait開始 B:lock内に進入 B:Pulse開始 B:lockから抜ける B:スレッド終了 A:Wait終了 A:lockから抜ける A:スレッド終了
この例では、ThreadClass
クラスのAメソッドとBメソッドを、別々のスレッドで実行します。まずAがクリティカルセクションに進入しますが、Monitor.Wait
によりロックが解除されます。このことにより、Bでクリティカルセクションに進入できるようになります。次にBからMonitor.Pulse
が呼び出されますが、これですぐにAのブロックが解除されるということはなく、Bがクリティカルセクションから抜けてロックが解放されてから、Aのブロックが解除されます。
ReaderWriterLockクラス
複数のスレッドから共有リソースへのアクセスを同期するためにはlock
を使用するのが基本ですが、読み取りアクセスに対しては複数のスレッドに許可を与え、書き込みアクセスに対しては一つのスレッドだけに許可を与えるという同期でよいのであれば、ReaderWriterLock
クラスを使用した方がより効率的です。よって、共有リソースからの読み取りが頻繁で、書き込みはめったになく、短時間で終了するようなケースでは、ReaderWriterLock
は有効な手段となります。
サンプル「readerwriterlock_01」では、ReaderWriterLock
クラスの基本的な使い方を紹介しています。
Class Class1 'ReaderWriterLockオブジェクトの作成 Private Shared rwl As New System.Threading.ReaderWriterLock '複数スレッドからアクセスする共通リソース Private Shared resource As String = "0123456789" 'エントリポイント Public Shared Sub Main() Dim t As System.Threading.Thread 'スレッドを作成し、開始する '作成するスレッドのうち半分は' '共有リソースから読み取るメソッドを 'もう半分は共有リソースに書き込むメソッドを実行する Dim i As Integer For i = 0 To 199 If i Mod 2 = 0 Then t = New System.Threading.Thread( _ New System.Threading.ThreadStart( _ AddressOf ReadFromResource)) t.Start() Else t = New System.Threading.Thread( _ New System.Threading.ThreadStart( _ AddressOf WriteToResource)) t.Start() End If Next i Console.ReadLine() End Sub '共有リソースから読み込む Private Shared Sub ReadFromResource() 'リーダーロックを取得 rwl.AcquireReaderLock(System.Threading.Timeout.Infinite) '共有リソースからの読み取りがスレッドセーフ Console.WriteLine(resource) 'ロックカウントをデクリメント '0になるとロックが解放される rwl.ReleaseReaderLock() End Sub '共有リソースに書き込む Private Shared Sub WriteToResource() 'ライタロックを取得 rwl.AcquireWriterLock(System.Threading.Timeout.Infinite) '共有リソースへの書き込み(読み取りも)がスレッドセーフ Dim s As String = resource.Substring(0, 1) resource = resource.Substring(1) System.Threading.Thread.Sleep(1) resource += s 'ライタロックカウントをデクリメント '0になるとロックが解放される rwl.ReleaseWriterLock() End Sub End Class
class Class1 { //ReaderWriterLockオブジェクトの作成 private static System.Threading.ReaderWriterLock rwl = new System.Threading.ReaderWriterLock(); //複数スレッドからアクセスする共通リソース private static string resource = "0123456789"; //エントリポイント public static void Main() { //スレッドを作成し、開始する //作成するスレッドのうち半分は //共有リソースから読み取るメソッドを //もう半分は共有リソースに書き込むメソッドを実行する for (int i = 0; i < 200; i++) { if (i % 2 == 0) { (new System.Threading.Thread( new System.Threading.ThreadStart( ReadFromResource))).Start(); } else { (new System.Threading.Thread( new System.Threading.ThreadStart( WriteToResource))).Start(); } } Console.ReadLine(); } //共有リソースから読み込む private static void ReadFromResource() { //リーダーロックを取得 rwl.AcquireReaderLock(System.Threading.Timeout.Infinite); //共有リソースからの読み取りがスレッドセーフ Console.WriteLine(resource); //ロックカウントをデクリメント //0になるとロックが解放される rwl.ReleaseReaderLock(); } //共有リソースに書き込む private static void WriteToResource() { //ライタロックを取得 rwl.AcquireWriterLock(System.Threading.Timeout.Infinite); //共有リソースへの書き込み(読み取りも)がスレッドセーフ string s = resource.Substring(0, 1); resource = resource.Substring(1); System.Threading.Thread.Sleep(1); resource += s; //ライタロックカウントをデクリメント //0になるとロックが解放される rwl.ReleaseWriterLock(); } }
スレッドが読み取りのためのロック(リーダーロック)を取得する時は、ReaderWriterLock.AcquireReaderLock
メソッドを、書き込みのためのロック(ライタロック)を取得する時は、ReaderWriterLock.AcquireWriterLock
メソッドを呼び出します。つまり、共有リソースから読み込む箇所はAcquireReaderLock
とReleaseReaderLock
で囲み、共有リソースに書き込む箇所はAcquireWriterLock
とReleaseWriterLock
で囲むというのが、基本的な使い方となります。
Mutexクラス
パート2の待機ハンドルの解説で触れたように、ここでMutex
(ミューテックス)クラスについて説明します。Mutex
クラスは、Monitor.Enter
・Exit
と同じような使い方ができます。まずはサンプル「mutex_01」をご覧ください。
Class Class1 Public Shared mut As System.Threading.Mutex 'エントリポイント Public Shared Sub Main() 'Mutexオブジェクトを作成(初期所有権なし) mut = New System.Threading.Mutex '2つのスレッドを作成し、開始する Dim t1 As New System.Threading.Thread( _ New System.Threading.ThreadStart(AddressOf DoSomething)) t1.Name = "1" Dim t2 As New System.Threading.Thread( _ New System.Threading.ThreadStart(AddressOf DoSomething)) t2.Name = "2" t1.Start() t2.Start() Console.ReadLine() End Sub Public Shared Sub DoSomething() Dim i As Integer For i = 0 To 9 'ミューテックスの所有権を要求する '取得できない時は、取得できるまで待機 mut.WaitOne() '同期されたコードブロック Console.WriteLine _ (System.Threading.Thread.CurrentThread.Name) 'Mutexを解放する mut.ReleaseMutex() Next i End Sub End Class
class Class1 { public static System.Threading.Mutex mut; //エントリポイント public static void Main() { //Mutexオブジェクトを作成(初期所有権なし) mut = new System.Threading.Mutex(); //2つのスレッドを作成し、開始する System.Threading.Thread t1 = new System.Threading.Thread( new System.Threading.ThreadStart(DoSomething)); t1.Name = "1"; System.Threading.Thread t2 = new System.Threading.Thread( new System.Threading.ThreadStart(DoSomething)); t2.Name = "2"; t1.Start(); t2.Start(); Console.ReadLine(); } public static void DoSomething() { for (int i = 0; i < 10; i++) { //ミューテックスの所有権を要求する //取得できない時は、取得できるまで待機 mut.WaitOne(); //同期されたコードブロック Console.WriteLine (System.Threading.Thread.CurrentThread.Name); //Mutexを解放する mut.ReleaseMutex(); } } }
この例では、DoSomething
メソッドのMutex.WaitOne
とMutex.ReleaseMutex
で囲まれた箇所がクリティカルセクションとなります。Mutex
は、同時に1つのスレッドでしか所有できない同期オブジェクトです。Mutex
を使用して同期を行うには、次のようにします。
まず、ミューテックスの所有権を持たないスレッドがWaitOne
メソッドを呼び出して所有権を要求します。この時ミューテックスの所有権を持つスレッドがなければ、このスレッドが所有権を取得しますが、別のスレッドが所有権を取得していれば、WaitOne
メソッドを呼び出したスレッドは待機状態になります。ミューテックスを所有しているスレッドがReleaseMutex
メソッドを呼び出すか、あるいは正常終了してミューテックスの所有を解放すると、待機中の次のスレッドが所有権を取得します。
Mutex
の大きな特徴は、スレッド間の同期だけではなく、プロセス間の同期にも使用できることです。上記の例は同一プロセス内のスレッドを同期するものでしたが、実際にはこのようなケースではMonitor
を使うべきであり、Mutex
を使うべきではありません。Mutex
はプロセス間の同期のみに使用するべきです。
Mutex
は、アプリケーションの二重起動を防ぐためによく使用されます。その例を以下に示します。
Class Class1 Private Shared _mutex As System.Threading.Mutex Shared Sub Main() 'Mutexクラスの作成 '"MyName"の部分を適当な文字列に変えてください _mutex = New System.Threading.Mutex(False, "MyName") 'ミューテックスの所有権を要求する If _mutex.WaitOne(0, False) = False Then 'すでに起動していると判断する Console.WriteLine("すでに起動しています。") '終了させるコードを書いてください Return End If Console.WriteLine("Enterキーで終了") Console.ReadLine() End Sub End Class
class Class1 { private static System.Threading.Mutex _mutex; static void Main() { //Mutexクラスの作成 //"MyName"の部分を適当な文字列に変えてください _mutex = new System.Threading.Mutex(false, "MyName"); //ミューテックスの所有権を要求する if (_mutex.WaitOne(0, false) == false) { //すでに起動していると判断する Console.WriteLine("すでに起動しています。"); //終了させるコードを書いてください return; } Console.WriteLine("Enterキーで終了"); Console.ReadLine(); } }
このアプリケーションを起動すると、「Enterキーで終了」と表示され、待機します。これを終了させずにもう一度アプリケーションを起動すると、「すでに起動しています。」と表示され、すぐに終了します。
プロセス間でMutex
を使用する場合は、適当なMutex
の名前を指定する必要があります。名前の付け方に関して詳しくは、MSDNライブラリの「CreateMutex」をご覧ください。
スレッドセーフなコレクション
Synchronizedメソッド
ArrayList
、Queue
などのコレクションクラスのインスタンスメンバは、スレッドセーフの保障がありません。ただし、ArrayList
、Hashtable
、Queue
、SortedList
、Stack
クラスには、スレッドセーフにする方法が用意されています。その方法とは、Synchronized
メソッドを使ってスレッドセーフが保障されたラッパーを作成し、このラッパーを使用することです。
ArrayList
、SortedList
クラスは内容の読み取りはスレッドセーフですが、コレクションの内容が変更される場合は、スレッドセーフではありません。Hashtable
クラスは読み取りに加え、単一の書き込み操作に関してはスレッドセーフです。 サンプル「collection_01」では、ArrayList
クラスのスレッドセーフなラッパーを作成する例を紹介しています。
'同期されていないArrayListオブジェクトの作成 Dim al As New System.Collections.ArrayList 'alを基にした同期されたArrayListラッパーの作成 Dim syncdAl As System.Collections.ArrayList = _ System.Collections.ArrayList.Synchronized(al)
//同期されていないArrayListオブジェクトの作成 System.Collections.ArrayList al = new System.Collections.ArrayList(); //alを基にした同期されたArrayListラッパーの作成 System.Collections.ArrayList syncdAl = System.Collections.ArrayList.Synchronized(al);
al
はスレッドセーフではありませんが、syncAl
はスレッドセーフとなります。スレッドセーフなラッパーをいきなり作成するには、次のようにすることもできます。
'同期されたArrayListの作成 syncdAl = System.Collections.ArrayList.Synchronized( _ New System.Collections.ArrayList)
//同期されたArrayListの作成 syncdAl =System.Collections.ArrayList.Synchronized( new System.Collections.ArrayList());
Array
クラスのようにSynchronized
メソッドがないコレクションでは、lock
を使用してスレッドセーフにします。IsSynchronizedプロパティ
サンプル「collection_01」では、IsSynchronized
プロパティも使用しています。
'syncdAlへのアクセスが同期されているか調べる If syncdAl.IsSynchronized Then Console.WriteLine("syncdAlへのアクセスが同期されています") Else Console.WriteLine("syncdAlへのアクセスが同期されていません") End If
//syncdAlへのアクセスが同期されているか調べる if (syncdAl.IsSynchronized) { Console.WriteLine("syncdAlへのアクセスが同期されています"); } else { Console.WriteLine("syncdAlへのアクセスが同期されていません"); }
IsSynchronized
メソッドを使うことにより、そのコレクションへのアクセスが同期されているか調べることができます。サンプル「collection_01」では、「syncdAlへのアクセスが同期されています」と表示されます。
コレクションの列挙処理
コレクションの列挙処理は、同期されたラッパーを使用してもスレッドセーフではありません。列挙処理をスレッドセーフに行うには、lock
(VB.NETではSyncLock
)を使用します。この時、コレクションのSyncRoot
プロパティを使ってロックします。
サンプル「collection_02」では、lock
を使わずにコレクションの列挙を行っています。そのため、このアプリケーションを実行すると、エラーが発生します。
サンプル「collection_03」では、lock
を使用してコレクションの列挙処理をスレッドセーフに行い、エラーが発生しないようにしています。
Class Class1 Private Shared syncdAl As System.Collections.ArrayList 'エントリポイント Public Shared Sub Main() '同期されたArrayListの作成 syncdAl = System.Collections.ArrayList.Synchronized( _ New System.Collections.ArrayList) 'ArrayListに要素を追加する Dim i As Integer For i = 0 To 99 syncdAl.Add(i) Next i 'スレッドの作成 Dim t As New System.Threading.Thread( _ New System.Threading.ThreadStart(AddressOf MyThread)) 'スレッドの開始 t.Start() 'syncdAlを変更する syncdAl.RemoveAt(0) Console.ReadLine() End Sub Private Shared Sub MyThread() 'syncdAlをロックする SyncLock syncdAl.SyncRoot '列挙処理をする Dim i As Integer For Each i In syncdAl Console.Write(i) Next i End SyncLock End Sub End Class
private static System.Collections.ArrayList syncdAl; //エントリポイント public static void Main() { //同期されたArrayListの作成 syncdAl =System.Collections.ArrayList.Synchronized( new System.Collections.ArrayList()); //ArrayListに要素を追加する for (int i = 0; i < 100; i++) { syncdAl.Add(i); } //スレッドの作成と開始 System.Threading.Thread t = new System.Threading.Thread( new System.Threading.ThreadStart(MyThread)); t.Start(); //syncdAlを変更する syncdAl.RemoveAt(0); Console.ReadLine(); } private static void MyThread() { //syncdAlをロックする lock(syncdAl.SyncRoot) { //列挙処理をする foreach (int i in syncdAl) { Console.Write(i); } } }
なお、TextReader
、TextWriter
、Match
、Group
クラスにもSynchronized
メソッドが存在します。これらもコレクションのSynchronized
メソッドと同様に、同期されたラッパーを返します。