上級プロフェッショナル向け:VB.NETにおける「ReaderWriterLockSlim」を活用した極限の並行処理
開発現場において、複数のスレッドから同時にアクセスされる共有キャッシュや、頻繁に参照されるインメモリのマスターデータを扱うとき、あなたはどう設計しているだろうか。
「とりあえず `SyncLock`(C#でいう `lock`)をかけておけば安全だ」
もしそう考えているなら、あなたのシステムは重大なスループットのボトルネックを抱えている可能性が高い。
`SyncLock` は万能の排他制御だが、それは「読み取り」と「書き込み」を完全に同等に扱い、一切の同時アクセスを許さないというブルドーザーのようなアプローチだ。
実務でよくある「99%が読み取りで、1%未満しか更新されないキャッシュデータ」において、 `SyncLock` を使うことは、ラッシュアワーの改札口を1つしか開けないようなものである。
今回は、VB.NETの底力を引き出し、マルチスレッド環境でのパフォーマンスを極限まで高めるための `System.Threading.ReaderWriterLockSlim` の実践的な活用法を伝授する。
—
1. なぜ `SyncLock` ではスケールしないのか?
業務システムにおいて、ファイルやデータベースから読み込んだ設定値やマスタデータをメモリ上に保持し、高速にアクセスさせたい要件は枚挙に暇がない。
ここで `SyncLock` を使用すると、次のような現象が起きる。
- スレッドAが「読み取り」のためにロックを取得している間、スレッドBが「同じ読み取り」をしたいだけなのに、強制的に待たされる。
- 読み取り専用の処理であるにもかかわらず、完全に直列化され、CPUのコア数が無駄になる。
これでは、スレッド数を増やせば増やすほど待ち時間が増えるという、マルチスレッドの恩恵を完全にスポイルした状態に陥る。
リーダー・ライターロックの本質
ここで登場するのが ReaderWriterLockSlim だ。このクラスは、アクセス権を以下の2つに分離する。
1. リーダー(読み取り専用): 他のリーダーと同時にリソースにアクセスできる(多重化)。ただし、ライターがいる時は待つ。
2. ライター(書き込み): 完全な排他制御。他のいかなるリーダーもライターも存在しない状態でのみ実行される。
この制御を入れるだけで、参照頻度の高いシステムのスループットは劇的に向上する。
—
2. 堅牢なキャッシュリポジトリの実装(プロダクションコード)
それでは、実際の業務アプリケーションでそのまま組み込める、スレッドセーフなキャッシュ管理クラスのコードを示す。
インメモリの辞書(Dictionary)に対し、`ReaderWriterLockSlim` を用いて極限まで無駄を削ぎ落とした実装だ。
Imports System.Threading
Imports System.Collections.Generic
Namespace Enterprise.Threading.Cache
”’
”’
Public NotInheritable Class OptimizedMemoryCache(Of TKey, TValue)
Implements IDisposable
Private ReadOnly _cache As Dictionary(Of TKey, TValue)
Private ReadOnly _lock As New ReaderWriterLockSlim(LockRecursionPolicy.NoRecursion)
Private _disposed As Boolean = False
Public Sub New()
_cache = New Dictionary(Of TKey, TValue)()
End Sub
”’
”’
Public Function GetValue(key As TKey, ByRef value As TValue) As Boolean
‘ 読み取りロックに入ります
_lock.EnterReadLock()
Try
Return _cache.TryGetValue(key, value)
Finally
‘ 確実にロックを解放します(非常に重要)
_lock.ExitReadLock()
End Try
End Function
”’
”’
Public Sub SetValue(key As TKey, value As TValue)
‘ 書き込みロックに入ります
_lock.EnterWriteLock()
Try
_cache(key) = value
Finally
_lock.ExitWriteLock()
End Sub
End Sub
”’
”’ 「まず存在確認(読込)し、なければ追加する(書込)」という処理の競合を防ぎます
Failsafeなパターンを実装します。
”’
Public Sub AddOrUpdate(key As TKey, valueFactory As Func(Of TKey, TValue))
‘ 最初はアップグレード可能ロックとして入る(読み取りつつ、後で書き込みに昇格できる)
_lock.EnterUpgradeableReadLock()
Try
Dim tempVal As TValue = Nothing
If _cache.TryGetValue(key, tempVal) Then
‘ 既に存在する場合は何もしない(あるいは更新ロジックを挟む)
Return
End If
‘ 存在しないため、書き込みロックにアップグレード
_lock.EnterWriteLock()
Try
‘ ダブルチェックイディオム(ロック切り替えの瞬間に他スレッドが書き込んでいる可能性があるため)
If Not _cache.ContainsKey(key) Then
_cache(key) = valueFactory(key)
End If
Finally
_lock.ExitWriteLock()
End Try
Finally
_lock.ExitUpgradeableReadLock()
End Try
End Sub
#Region “IDisposable Implementation”
Public Sub Dispose()
Dispose(True)
GC.SuppressFinalize(Me)
End Sub
Protected Overridable Sub Dispose(disposing As Boolean)
If Not _disposed Then
If disposing Then
‘ ReaderWriterLockSlimは必ず破棄する必要があります(アンマネージドリソースのリーク防止)
If _lock IsNot Nothing Then
_lock.Dispose()
End If
End If
_disposed = True
End If
End Sub
#End Region
End Class
End Namespace
—
3. コードの急所:プロフェッショナルが解説する設計のポイント
上記のコードには、単に動くだけではなく、過酷なエンタープライズ環境を生き抜くための知見が凝縮されている。
① `LockRecursionPolicy.NoRecursion` の明示
コンストラクタで `New ReaderWriterLockSlim(LockRecursionPolicy.NoRecursion)` を指定している点に注目してほしい。
デフォルトの再入可能(Recursion)設定は、同一スレッド内で二重にロックを取得できてしまい、コードの構造が複雑化しデッドロックやパフォーマンス低下の温床になる。
「同じスレッド内であってもロックの再取得は禁止する」という制約をあえて課すことで、設計の美しさとパフォーマンスを担保する。これがプロの選択だ。
② `Try…Finally` による確実な解放
スレッドが例外をスローした場合でも、ロックが解放されずにプロセス全体がデッドロックする事故は、現場で最も恐れられるバグの一つだ。
VB.NETの `Try…Finally` 構文を使い、ロックの取得直後に `Finally` で確実に `ExitReadLock()` / `ExitWriteLock()` を呼ぶ構造を徹底すること。
③ アップグレード可能ロック(Upgradeable Read Lock)の活用
「データが存在するか確認し、なければマスタからロードして書き込む」という処理を素朴に実装すると、確認と書き込みの間に別のスレッドが割り込むレースコンディション(競合状態)が発生する。
かといって初めから `EnterWriteLock` を使うと、読み取りだけの時もボトルネックになる。
ここで `EnterUpgradeableReadLock()` を使うことで、「安全に読み取りつつ、必要に応じて排他書き込み権限に昇格する」という高度な芸技が可能になる。
—
4. ファイルやデータベース連携における注意点
このキャッシュクラスを実際の業務(CSVファイルの読み込みやDBからの初期ロード)に組み込む際の鉄則を伝えておく。
- ロック内での重いI/O処理の排除:
`EnterWriteLock` や `EnterReadLock` のブロック内で、ファイル読み込みやSQLの実行を行ってはならない。ロック保持時間が長くなると、せっかくの並行処理のメリットが相殺され、スレッドプールが枯渇する。
必ずロックの外側でデータを取得・生成してから、キャッシュへのマージ(書き込み)の瞬間だけ短い時間ロックを取得すること。
- 破棄(Dispose)の徹底:
`ReaderWriterLockSlim` は内部でOSの同期プリミティブを保持しているため、不要になったら確実に `Dispose()` しなければならない。アプリケーションのライフサイクルに合わせたDIコンテナでの管理か、Using構文の徹底が不可欠だ。
—
総括
VB.NETは、レガシーな言語と誤解されることがあるが、.NETの強力なランタイム(CLR)の機能をそのまま、かつスマートに記述できる優れた言語である。
`SyncLock` の手軽さに逃げるのではなく、データの特性(読み書きの比率)を見極め、`ReaderWriterLockSlim` を適切に配置する。この細部へのこだわりこそが、過負荷な状況下でも秒速で応答し続ける「真に堅牢な業務システム」を構築する唯一の道である。
次の設計からは、ぜひこの知見をコードに落とし込んでみてほしい。
