#title(.NETプログラミング研究 第26号) #contents *.NETプログラミング研究 第26号 [#e158a430] **.NET Tips [#q56b20c3] **.NETのマルチスレッドプログラミング その8 [#q89251a1] #column(注意){{ この記事の最新版は「[[.NETのマルチスレッドプログラミング>http://dobon.net/vb/dotnet/programing/multithread.html]]」で公開しています。 }} .NETのマルチスレッドプログラミングについて、あれやこれやと書いてきましたが、主なところは一通り紹介しきったのではないかと思いますので(それ以上に書く方も読む方も飽きたでしょう)、今回でひとまず終わりにしたいと考えています。今回は今まで紹介していなかった小ネタをできるだけ紹介するつもりですが、紹介し切れなかった事柄については、後々[[DOBON.NET>http://dobon.net]]にアップする予定です。 ***ReaderWriterLockの使い方 [#qb494362] ReaderWriterLockは、複数スレッドからの共有リソースへのアクセスを同期するために使用します。Monitorを使用しての同期と違い、ReaderWriterLockは読み込みアクセスに対して複数のスレッドに許可を与え、書き込みアクセスに対しては一つのスレッドにしか許可しません。よって、共有リソースへの書き込みが頻繁で、書き込みはめったになく、かつ短時間で終了するようなケースではReaderWriterLockは有効な手段となります。 スレッドが読み込みのためのロック(リーダーロック)を取得する時は、ReaderWriterLock.AcquireReaderLockメソッドを、書き込みのためのロック(ライタロック)を取得する時は、ReaderWriterLock.AcquireWriterLockメソッドを呼び出します。 以下にReaderWriterLockを使用した簡単な例を示します。このコードのReaderWriterLockを使っている箇所を削除すると同期が取れず、大変なことになります。 #code(vbnet){{ 'Imports System.Threading 'が宣言されているものとする 'ReaderWriterLockオブジェクトの作成 Private Shared rwl As New ReaderWriterLock '複数スレッドからアクセスする共通リソース Private Shared resource As String = "0123456789" 'エントリポイント Public Shared Sub Main() 'スレッドを作成し、開始する '作成するスレッドのうち半分は共有リソースから読み取るメソッドを 'もう半分は共有リソースに書き込むメソッドを実行する Dim i As Integer For i = 0 To 199 If i Mod 2 = 0 Then Dim t As New Thread( _ New ThreadStart(AddressOf ReadFromResource)) t.Start() Else Dim t As New Thread( _ New ThreadStart(AddressOf WriteToResource)) t.Start() End If Next i Console.ReadLine() End Sub '共有リソースから読み込む Private Shared Sub ReadFromResource() 'リーダーロックを取得 rwl.AcquireReaderLock(Timeout.Infinite) '共有リソースからの読み込みがスレッドセーフ Console.WriteLine(resource) 'ロックカウントをデクリメント '0になるとロックが解放される rwl.ReleaseReaderLock() End Sub '共有リソースに書き込む Private Shared Sub WriteToResource() 'ライタロックを取得 rwl.AcquireWriterLock(Timeout.Infinite) '共有リソースへの書き込み(読み込みも)がスレッドセーフ Dim s As String = resource.Substring(0, 1) resource = resource.Substring(1) Thread.Sleep(1) resource += s 'ライタロックカウントをデクリメント '0になるとロックが解放される rwl.ReleaseWriterLock() End Sub }} #code(csharp){{ //using System.Threading; //が宣言されているものとする //ReaderWriterLockオブジェクトの作成 private static ReaderWriterLock rwl = new ReaderWriterLock(); //複数スレッドからアクセスする共通リソース private static string resource = "0123456789"; //エントリポイント public static void Main() { //スレッドを作成し、開始する //作成するスレッドのうち半分は共有リソースから読み取るメソッドを //もう半分は共有リソースに書き込むメソッドを実行する for (int i = 0; i < 200; i++) { if (i % 2 == 0) { (new Thread(new ThreadStart(ReadFromResource))).Start(); } else { (new Thread(new ThreadStart(WriteToResource))).Start(); } } Console.ReadLine(); } //共有リソースから読み込む private static void ReadFromResource() { //リーダーロックを取得 rwl.AcquireReaderLock(Timeout.Infinite); //共有リソースからの読み込みがスレッドセーフ Console.WriteLine(resource); //ロックカウントをデクリメント //0になるとロックが解放される rwl.ReleaseReaderLock(); } //共有リソースに書き込む private static void WriteToResource() { //ライタロックを取得 rwl.AcquireWriterLock(Timeout.Infinite); //共有リソースへの書き込み(読み込みも)がスレッドセーフ string s = resource.Substring(0, 1); resource = resource.Substring(1); Thread.Sleep(1); resource += s; //ライタロックカウントをデクリメント //0になるとロックが解放される rwl.ReleaseWriterLock(); } }} リーダーロックを取得中に共有リソースへ書き込む必要が生じたときは、ReaderWriterLock.UpgradeToWriterLockメソッドを使ってライタロックへアップグレードすることができます。(ReaderWriterLock.UpgradeToWriterLockメソッドを呼び出したら、ReaderWriterLock.DowngradeFromWriterLockメソッドによりリーダーロックを復元します。) ReaderWriterLock.ReleaseLockメソッドでもロックを解放できますが、この時はスレッドがロックを取得した回数に関係なく、すぐにロックを解放します。ReleaseLockメソッドが返すLockCookieオブジェクトをRestoreLockメソッドに使うことにより、解放したロックを復元することもできます。このことを利用すれば、ロックを一時的に解放することができます。 さらに、WriterSeqNumプロパティで現在のライタシーケンス番号を覚えておき、それをAnyWritersSinceメソッドに使うことにより、その間にライタロックを取得したスレッドがあるか調べることができます。もしライタロックを取得したスレッドがあれば、共有リソースの内容が変更されたと判断できます。これを利用すれば、例えば、リーダーロック解放前に共有リソースの内容をローカル変数などに保存しておき、その後再びリーダーロックを取得してから共有リソースの内容が変更されていれば共有リソースを読み込み、変更されていなければ保存してある内容を使用するということができます。 次にUpgradeToWriterLock、DowngradeFromWriterLockメソッド、ReleaseLockメソッド、RestoreLockメソッド、WriterSeqNumプロパティ、AnyWritersSinceメソッドの具体的な使用例を示します。UpAndDownGradeメソッドでUpgradeToWriterLock、DowngradeFromWriterLockメソッドを、ReleaseAndRestoreメソッドでReleaseLockメソッド、RestoreLockメソッド、WriterSeqNumプロパティ、AnyWritersSinceメソッドを使っています。 #code(vbnet){{ 'Imports System.Threading 'が宣言されているものとする 'ReaderWriterLockオブジェクトの作成 Private Shared rwl As New ReaderWriterLock '複数スレッドからアクセスする共通リソース Private Shared resource As String = "0123456789" 'エントリポイント Public Shared Sub Main() 'スレッドを作成し、開始する Dim i As Integer For i = 0 To 199 If i Mod 2 = 0 Then Dim t As New Thread( _ New ThreadStart(AddressOf UpAndDownGrade)) t.Start() Else Dim t As New Thread( _ New ThreadStart(AddressOf ReleaseAndRestore)) t.Start() End If Next i Console.ReadLine() End Sub '共有リソースを読み書きする Private Shared Sub UpAndDownGrade() 'リーダーロックを取得 rwl.AcquireReaderLock(Timeout.Infinite) '共有リソースからの読み取りがスレッドセーフ Console.WriteLine(resource) 'ライタロックにアップグレード Dim lc As LockCookie = rwl.UpgradeToWriterLock(Timeout.Infinite) '共有リソースへの書き込み(読み込みも)がスレッドセーフ Dim s As String = resource.Substring(0, 1) resource = resource.Substring(1) Thread.Sleep(1) resource += s 'リーダーロックに戻す rwl.DowngradeFromWriterLock(lc) 'ロックカウントをデクリメント '0になるとロックが解放される rwl.ReleaseReaderLock() End Sub 'ロックを解放し、復元する Private Shared Sub ReleaseAndRestore() 'リーダーロックを取得 rwl.AcquireReaderLock(Timeout.Infinite) '共有リソースからの読み取りがスレッドセーフ '共有リソースの内容を読み取る Dim resourceValue As String = resource '現在のライタシーケンス番号を覚えておく Dim seqNum As Integer = rwl.WriterSeqNum 'ロックを解放する '後で復元するため、LockCookieを記憶しておく Dim lc As LockCookie = rwl.ReleaseLock() 'ロックが解放されている 'ちょっと間を空ける Thread.Sleep(10) 'ロックを復元する rwl.RestoreLock(lc) 'ロック開放中に他のスレッドがライタロックを取得したか調べる 'trueならば共有リソースが変更されたとみなす If rwl.AnyWritersSince(seqNum) Then Console.WriteLine("({0})", resourceValue) '共有リソースから読み込む resourceValue = resource End If '表示する Console.WriteLine(resourceValue) 'ロックカウントをデクリメント '0になるとロックが解放される rwl.ReleaseReaderLock() End Sub }} #code(csharp){{ //using System.Threading; //が宣言されているものとする //ReaderWriterLockオブジェクトの作成 private static ReaderWriterLock rwl = new ReaderWriterLock(); //複数スレッドからアクセスする共通リソース private static string resource = "0123456789"; //エントリポイント public static void Main() { //スレッドを作成し、開始する for (int i = 0; i < 200; i++) { if (i % 2 == 0) { (new Thread(new ThreadStart(UpAndDownGrade))).Start(); } else { (new Thread(new ThreadStart(ReleaseAndRestore))).Start(); } } Console.ReadLine(); } //共有リソースを読み書きする private static void UpAndDownGrade() { //リーダーロックを取得 rwl.AcquireReaderLock(Timeout.Infinite); //共有リソースからの読み取りがスレッドセーフ Console.WriteLine(resource); //ライタロックにアップグレード LockCookie lc = rwl.UpgradeToWriterLock(Timeout.Infinite); //共有リソースへの書き込み(読み込みも)がスレッドセーフ string s = resource.Substring(0, 1); resource = resource.Substring(1); Thread.Sleep(1); resource += s; //リーダーロックに戻す rwl.DowngradeFromWriterLock(ref lc); //ロックカウントをデクリメント //0になるとロックが解放される rwl.ReleaseReaderLock(); } //ロックを解放し、復元する private static void ReleaseAndRestore() { //リーダーロックを取得 rwl.AcquireReaderLock(Timeout.Infinite); //共有リソースからの読み取りがスレッドセーフ //共有リソースの内容を読み取る string resourceValue = resource; //現在のライタシーケンス番号を覚えておく int seqNum = rwl.WriterSeqNum; //ロックを解放する //後で復元するため、LockCookieを記憶しておく LockCookie lc = rwl.ReleaseLock(); //ロックが解放されている //ちょっと間を空ける Thread.Sleep(10); //ロックを復元する rwl.RestoreLock(ref lc); //ロック開放中に他のスレッドがライタロックを取得したか調べる //trueならば共有リソースが変更されたとみなす if (rwl.AnyWritersSince(seqNum)) { Console.WriteLine("({0})", resourceValue); //共有リソースから読み込む resourceValue = resource; } //表示する Console.WriteLine(resourceValue); //ロックカウントをデクリメント //0になるとロックが解放される rwl.ReleaseReaderLock(); } }} ***スレッドタイマの使い方 [#vbda9b77] .NET Frameworkで用意されているタイマには、System.Windows.Forms.Timer(Windowsタイマ)、System.Threading.Timer(スレッドタイマ)、System.Timers.Timer(サーバータイマ)の3種類があります。Windowsフォームアプリケーションの開発ではWindowsタイマがよく使われますが、これはOSのタイマ機能を使用しているため、スレッドでメッセージを処理しなければタイマは発生せず、長い処理によりブロックされる恐れがあります。これに対してサーバータイマとスレッドタイマはワーカースレッドにより処理されますので、その心配がありません。 サーバータイマについてはヘルプによると、「サーバー ベースのタイマは、サーバー環境での実行用に最適化された従来のタイマを強化したものです。」とのことです。Visual Studio .NETのツールボックスの「コンポーネント」タブにあるのはこれです。 スレッドタイマは、システムが提供するスレッドプールを使用したタイマです。イベントの代わりにコールバックメソッドを使用します。軽量で、簡単なタイマ処理が必要な時に便利です。(これらのタイマの違いに関する詳細は、ヘルプの「サーバー ベースのタイマの概説」等をご覧ください。) -[[サーバー ベースのタイマの概説>http://www.microsoft.com/japan/msdn/library/ja/vbcon/html/vbconserverbasedtimers.asp]] スレッドタイマThreading.Timerを作成するには、コンストラクタで実行するメソッドへのTimerCallbackデリゲート、メソッドで使用される状態オブジェクト、最初に発生する時間、発生する時間間隔を渡します。最初に発生する時間、発生する時間間隔を変更するには、Changeメソッドを使います。タイマをキャンセルするには、Timer.Dispose関数を呼び出します。なお、Dispose関数を呼び出せるように、Threading.Timerオブジェクトへの参照は保持しておくべきです。 下のコードでは、TimerState.Tickメソッドをスレッドタイマにより、定期的に実行します。まず0.1秒後に1秒間隔で実行し、TimerState.Tickメソッドが5回呼び出されると、0.5秒間隔で実行するように変更します。そして10回呼び出されたところで、タイマを破棄します。 #code(vbnet){{ 'Imports System.Threading 'が宣言されているものとする 'マルチスレッドメソッドのためのクラス 'using System.Threading; 'が宣言されているものとする Class TimerState Private Count As Integer = 0 Public ThreadTimer As System.Threading.Timer Public Sub Tick(ByVal state As Object) '状態オブジェクトの取得 Dim ts As TimerState = CType(state, TimerState) 'カウンタをインクリメント Count += 1 '表示 Console.WriteLine("システム起動後の経過時間:{0}ミリ秒({1})", _ System.Environment.TickCount, Count) If Count = 5 Then '5回呼び出されたとき、0.5秒おきに実行されるように変更する ts.ThreadTimer.Change(500, 500) Console.WriteLine("Changed") Else If Count = 10 Then '10回呼び出されたときは、タイマを破棄する ts.ThreadTimer.Dispose() ts.ThreadTimer = Nothing Console.WriteLine("Disposed") End If End If End Sub End Class Class MainClass 'エントリポイント Public Shared Sub Main() Dim ts As New TimerState 'Threading.Timerオブジェクトの作成 'TimerState.Tickを0.1秒後に1秒間隔で実行する ts.ThreadTimer = New System.Threading.Timer( _ New TimerCallback(AddressOf ts.Tick), ts, 100, 1000) '待機する Console.ReadLine() End Sub End Class }} #code(csharp){{ //using System.Threading; //が宣言されているものとする class TimerState { private int Count = 0; public System.Threading.Timer ThreadTimer; public void Tick(object state) { //状態オブジェクトの取得 TimerState ts = (TimerState) state; //カウンタをインクリメント Count++; //表示 Console.WriteLine("システム起動後の経過時間:{0}ミリ秒({1})", System.Environment.TickCount, Count); if (Count == 5) { //5回呼び出されたとき、0.5秒おきに実行されるように変更する ts.ThreadTimer.Change(500, 500); Console.WriteLine("Changed"); } else if (Count == 10) { //10回呼び出されたときは、タイマを破棄する ts.ThreadTimer.Dispose(); ts.ThreadTimer = null; Console.WriteLine("Disposed"); } } } class MainClass { //エントリポイント public static void Main() { TimerState ts = new TimerState(); //Threading.Timerオブジェクトの作成 //TimerState.Tickを0.1秒後に1秒間隔で実行する ts.ThreadTimer = new System.Threading.Timer( new TimerCallback(ts.Tick), ts, 100, 1000); //待機する Console.ReadLine(); } } }} 上記コードの結果は例えば次のようになります。 #pre{{ システム起動後の経過時間:8315236ミリ秒(1) システム起動後の経過時間:8316218ミリ秒(2) システム起動後の経過時間:8317219ミリ秒(3) システム起動後の経過時間:8318220ミリ秒(4) システム起動後の経過時間:8319222ミリ秒(5) Changed システム起動後の経過時間:8319723ミリ秒(6) システム起動後の経過時間:8320223ミリ秒(7) システム起動後の経過時間:8320724ミリ秒(8) システム起動後の経過時間:8321225ミリ秒(9) システム起動後の経過時間:8321726ミリ秒(10) Disposed }} 参考: -[[タイマ>http://www.microsoft.com/japan/msdn/library/ja/cpguide/html/cpcontimer.asp]] -[[スレッド タイマ>http://www.microsoft.com/japan/msdn/library/ja/vbcn7/html/vaconthreadtimers.asp]] -[[Timer クラス>http://www.microsoft.com/japan/msdn/library/ja/cpref/html/frlrfsystemthreadingtimerclasstopic.asp]] ***スレッドセーフなコレクションを使う [#c1a159cd] System.Collections名前空間にあるコレクションクラス(ArrayList、Queueなど)のインスタンスメンバは、スレッドセーフである保障がありません。複数のスレッドがコレクションの内容を読み取る限りにおいては問題ありませんが、コレクションの内容が変更されうる場合は、コレクションにアクセスするすべてのスレッドでその結果は保障されません。(ただしHashtableクラスは単一の書き込み操作に関してはスレッドセーフが保障されています。) コレクションをスレッドセーフにするためには、コレクションのSynchronizedメソッドによりスレッドセーフラッパーを作成し、そのラッパーを通じてコレクションにアクセスするようにします。コレクションがスレッドセーフであるか確かめるには、IsSynchronizedプロパティがtrueかどうか調べます。 次にSynchronizedメソッドを使用した例を示します。 #code(vbnet){{ 'using System.Collections; 'が宣言されているものとする '同期されていないArrayListオブジェクトの作成 Dim al As New ArrayList 'alを同期するArrayListラッパーの取得 Dim syncdAl As ArrayList = ArrayList.Synchronized(al) '同期されたArrayListオブジェクトの作成 Dim syncdAl2 As ArrayList = _ ArrayList.Synchronized(New ArrayList) '同期されているか確かめる Console.WriteLine("al.IsSynchronized = {0}", _ al.IsSynchronized) Console.WriteLine("syncdAl.IsSynchronized = {0}", _ syncdAl.IsSynchronized) Console.WriteLine("syncdAl2.IsSynchronized = {0}", _ syncdAl2.IsSynchronized) }} #code(csharp){{ //using System.Collections; //が宣言されているものとする //同期されていないArrayListオブジェクトの作成 ArrayList al = new ArrayList(); //alを同期するArrayListラッパーの取得 ArrayList syncdAl = ArrayList.Synchronized(al); //同期されたArrayListオブジェクトの作成 ArrayList syncdAl2 = ArrayList.Synchronized(new ArrayList()); //同期されているか確かめる Console.WriteLine("al.IsSynchronized = {0}", al.IsSynchronized); Console.WriteLine("syncdAl.IsSynchronized = {0}", syncdAl.IsSynchronized); Console.WriteLine("syncdAl2.IsSynchronized = {0}", syncdAl2.IsSynchronized); }} #pre{{ 出力結果; al.IsSynchronized = False syncdAl.IsSynchronized = True syncdAl2.IsSynchronized = True }} コレクションの列挙はスレッドセーフな処理ではありませんので、コレクションが同期されていても別のスレッドがコレクションの内容を変更することがあり得るため、その結果例外をスローするかもしれません。列挙処理をスレッドセーフに行うには、列挙処理中コレクションをlock(VB.NETではSyncLock)などによりロックします。 lockでコレクションをロックする時は、そのコレクション自身ではなく、コレクションのSyncRootプロパティを使用したほうがよいでしょう。(スレッドセーフな独自のラッパーを作成する時にも、派生クラスでSyncRootプロパティが使用されます。) 次にコレクションの列挙処理をスレッドセーフに行う例を示します。 #code(vbnet){{ 'using System.Collections; 'が宣言されているものとする '同期されたArrayList Private Shared syncdAl As ArrayList = _ ArrayList.Synchronized(New ArrayList) 'エントリポイント Public Shared Sub Main() Dim i As Integer For i = 0 To 99 syncdAl.Add(i) Next i 'スレッドの作成と開始 Dim t As New Thread(New ThreadStart(AddressOf MyThread)) t.Start() 'ちょっと待ってから、syncdAlを変更する Thread.Sleep(500) 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) Thread.Sleep(10) Next i End SyncLock End Sub }} #code(csharp){{ //using System.Collections; //が宣言されているものとする //同期されたArrayList private static ArrayList syncdAl = ArrayList.Synchronized(new ArrayList()); //エントリポイント public static void Main() { for (int i = 0; i < 100; i++) { syncdAl.Add(i); } //スレッドの作成と開始 Thread t = new Thread(new ThreadStart(MyThread)); t.Start(); //ちょっと待ってから、syncdAlを変更する Thread.Sleep(500); syncdAl.RemoveAt(0); Console.ReadLine(); } private static void MyThread() { //syncdAlをロックする //これをしないと、例外がスローされる lock(syncdAl.SyncRoot) { //列挙処理をする foreach (int i in syncdAl) { Console.Write(i); Thread.Sleep(10); } } } }} ArrayクラスのようにSynchronizedメソッドがないコレクションでは、上記と同じように、lockを使用することによりスレッドセーフにすることができます。 参考: -[[コレクションと同期 (スレッド セーフ)>http://www.microsoft.com/japan/msdn/library/ja/cpguide/html/cpconcollectionssynchronizationthreadsafety.asp]] ***Interlockedの使い方 [#b33490fc] 以前「スレッドの同期」にて、「競合状態」の説明をしました。競合状態は「x++」のような単純な変数のインクリメントでも発生します。一見一回の操作のみで行われるように見える変数のインクリメントも実は、「変数の値を取得する → 1つ足す → 結果を変数に格納する」という幾つかの操作により行われており、あるスレッドが変数に1足して結果を格納する前に、別のスレッドが1足してしまうということが起こりうるのです。 lockステートメントによる同期により競合状態を防いでインクリメントを行うには、次のようにします。 #code(vbnet){{ SyncLock Me x += 1 End SyncLock }} #code(csharp){{ lock (this) { x++; } }} 実は.NET Frameworkではより優れた方法として、Interlocked.Incrementメソッドが用意されています。Interlocked.Incrementメソッドは、先に説明した変数インクリメントの一連の操作を、分割不可能な操作として一度に行ってくれるため、競合状態の発生する隙を与えません。(このように操作が分割不可能であることを「アトミック」と呼びます。) 上記のコードをInterlocked.Incrementメソッドで書き直すと、次のようになります。 #code(vbnet){{ System.Threading.Interlocked.Increment(x) }} #code(csharp){{ System.Threading.Interlocked.Increment(x); }} Interlockedクラスの4つの静的メソッドIncrement、Decrement、Exchange、CompareExchangeメソッドを使うことにより、整数のインクリメント、デクリメント、さらに変数への値の設定(変数の値の交換)、変数の比較と値の設定を分割不可能な操作として実行できます。 参考: -[[Interlocked>http://www.microsoft.com/japan/msdn/library/ja/cpguide/html/cpconinterlocked.asp]] -[[Interlocked クラス>http://www.microsoft.com/japan/msdn/library/ja/cpref/html/frlrfSystemThreadingInterlockedClassTopic.asp]] -[[スレッド処理のデザイン ガイドライン>http://www.microsoft.com/japan/msdn/library/ja/cpgenref/html/cpconthreadingdesignguidelines.asp]] **コメント [#x0ce129e] #comment //これより下は編集しないでください #pageinfo([[:Category/.NET]],2004-02-12 (木) 06:00:00,DOBON!,2010-03-21 (日) 01:18:21,DOBON!) |