注意この記事の最新版は「.NETのマルチスレッドプログラミング」で公開しています。.NETのマルチスレッドプログラミングについて、あれやこれやと書いてきましたが、主なところは一通り紹介しきったのではないかと思いますので(それ以上に書く方も読む方も飽きたでしょう)、今回でひとまず終わりにしたいと考えています。今回は今まで紹介していなかった小ネタをできるだけ紹介するつもりですが、紹介し切れなかった事柄については、後々DOBON.NETにアップする予定です。†ReaderWriterLockは、複数スレッドからの共有リソースへのアクセスを同期するために使用します。Monitorを使用しての同期と違い、ReaderWriterLockは読み込みアクセスに対して複数のスレッドに許可を与え、書き込みアクセスに対しては一つのスレッドにしか許可しません。よって、共有リソースへの書き込みが頻繁で、書き込みはめったになく、かつ短時間で終了するようなケースではReaderWriterLockは有効な手段となります。スレッドが読み込みのためのロック(リーダーロック)を取得する時は、ReaderWriterLock.AcquireReaderLockメソッドを、書き込みのためのロック(ライタロック)を取得する時は、ReaderWriterLock.AcquireWriterLockメソッドを呼び出します。以下にReaderWriterLockを使用した簡単な例を示します。このコードのReaderWriterLockを使っている箇所を削除すると同期が取れず、大変なことになります。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メソッドを使っています。Frameworkで用意されているタイマには、System.Windows.Forms.Timer(Windowsタイマ)、System.Threading.Timer(スレッドタイマ)、System.Timers.Timer(サーバータイマ)の3種類があります。Windowsフォームアプリケーションの開発ではWindowsタイマがよく使われますが、これはOSのタイマ機能を使用しているため、スレッドでメッセージを処理しなければタイマは発生せず、長い処理によりブロックされる恐れがあります。これに対してサーバータイマとスレッドタイマはワーカースレッドにより処理されますので、その心配がありません。ベースのタイマは、サーバー環境での実行用に最適化された従来のタイマを強化したものです。.NETのツールボックスの「コンポーネント」タブにあるのはこれです。スレッドタイマは、システムが提供するスレッドプールを使用したタイマです。イベントの代わりにコールバックメソッドを使用します。軽量で、簡単なタイマ処理が必要な時に便利です。ベースのタイマの概説」等をご覧ください。ベースのタイマの概説スレッドタイマThreading.Timerを作成するには、コンストラクタで実行するメソッドへのTimerCallbackデリゲート、メソッドで使用される状態オブジェクト、最初に発生する時間、発生する時間間隔を渡します。最初に発生する時間、発生する時間間隔を変更するには、Changeメソッドを使います。タイマをキャンセルするには、Timer.Dispose関数を呼び出します。Dispose関数を呼び出せるように、Threading.Timerオブジェクトへの参照は保持しておくべきです。下のコードでは、TimerState.Tickメソッドをスレッドタイマにより、定期的に実行します。0.1秒後に1秒間隔で実行し、TimerState.Tickメソッドが5回呼び出されると、0.5秒間隔で実行するように変更します。10回呼び出されたところで、タイマを破棄します。}}上記コードの結果は例えば次のようになります。†System.Collections名前空間にあるコレクションクラス(ArrayList、Queueなど)のインスタンスメンバは、スレッドセーフである保障がありません。複数のスレッドがコレクションの内容を読み取る限りにおいては問題ありませんが、コレクションの内容が変更されうる場合は、コレクションにアクセスするすべてのスレッドでその結果は保障されません。(ただしHashtableクラスは単一の書き込み操作に関してはスレッドセーフが保障されています。)コレクションをスレッドセーフにするためには、コレクションのSynchronizedメソッドによりスレッドセーフラッパーを作成し、そのラッパーを通じてコレクションにアクセスするようにします。コレクションがスレッドセーフであるか確かめるには、IsSynchronizedプロパティがtrueかどうか調べます。次にSynchronizedメソッドを使用した例を示します。Trueコレクションの列挙はスレッドセーフな処理ではありませんので、コレクションが同期されていても別のスレッドがコレクションの内容を変更することがあり得るため、その結果例外をスローするかもしれません。列挙処理をスレッドセーフに行うには、列挙処理中コレクションをlock(VB.NETではSyncLock)などによりロックします。lockでコレクションをロックする時は、そのコレクション自身ではなく、コレクションのSyncRootプロパティを使用したほうがよいでしょう。(スレッドセーフな独自のラッパーを作成する時にも、派生クラスでSyncRootプロパティが使用されます。)次にコレクションの列挙処理をスレッドセーフに行う例を示します。}}ArrayクラスのようにSynchronizedメソッドがないコレクションでは、上記と同じように、lockを使用することによりスレッドセーフにすることができます。以前「スレッドの同期」にて、「競合状態」の説明をしました。競合状態は「x++」のような単純な変数のインクリメントでも発生します。結果を変数に格納する」という幾つかの操作により行われており、あるスレッドが変数に1足して結果を格納する前に、別のスレッドが1足してしまうということが起こりうるのです。lockステートメントによる同期により競合状態を防いでインクリメントを行うには、次のようにします。Frameworkではより優れた方法として、Interlocked.Incrementメソッドが用意されています。Interlocked.Incrementメソッドは、先に説明した変数インクリメントの一連の操作を、分割不可能な操作として一度に行ってくれるため、競合状態の発生する隙を与えません。(このように操作が分割不可能であることを「アトミック」と呼びます。)上記のコードをInterlocked.Incrementメソッドで書き直すと、次のようになります。1System.Threading.Interlocked.Increment(x);Interlockedクラスの4つの静的メソッドIncrement、Decrement、Exchange、CompareExchangeメソッドを使うことにより、整数のインクリメント、デクリメント、さらに変数への値の設定(変数の値の交換)、変数の比較と値の設定を分割不可能な操作として実行できます。