VB.NETにおける「ReaderWriterLockSlim」を活用した極限の並行処理:読み取り専用スレッドの多重化と書き込み排他の最適化によるスループット向上
レガシーなVBAシステムからの脱却、あるいは社内ニッチを支える高負荷なVB.NET基幹バッチ・WebAPI群の構築において、避けて通れないのが「マルチスレッド環境下でのメモリ管理と排他制御」だ。
「とりあえず `SyncLock`(C#の `lock`)を張っておけば安全だ」——そう考えて実装した結果、高負荷時にスレッドが完全に直列化され、CPUコアが遊んでいるにもかかわらずスループットが極端に低下する。このアンチパターンに直面したシニアエンジニアは少なくないはずだ。
特に、「頻繁に参照(読み取り)され、極めて稀に更新(書き込み)される共有キャッシュ」を扱う場合、`SyncLock` は百害あって一利なしの悪手となる。読み取りスレッド同士であっても完全に排他されてしまうからだ。
今回は、VB.NETの底力を限界まで引き出し、CLR(Common Language Runtime)の同期プリミティブの真髄である `System.Threading.ReaderWriterLockSlim` を用いて、読み取りの並行性を極限まで高めつつ、書き込み時の安全性とスループットを両立させるアーキテクチャを解説する。
—
1. なぜ `SyncLock` ではスケールしないのか?
`SyncLock` の裏側では `Monitor.Enter` / `Monitor.Exit` が動いている。これは「排他制御の王道」ではあるが、「1つのリソースに対し、1つのスレッドしかアクセスを許さない」という非常に粗いロック粒度を持つ。
次のようなシチュエーションを想像してほしい。
- 対象: 数万件のマスタデータをメモリ上に保持するインメモリ・キャッシュ。
- アクセス頻度: 1秒間に数千回の「参照」。
- 更新頻度: 1時間に1回の「マスタ再読み込み(書き込み)」。
この状況で `SyncLock` を使うと、1秒間に数千ある読み取りリクエストが、完全に1つずつ直列に処理される。CPUのコンテキストスイッチのオーバーヘッドが増大し、システム全体のパフォーマンスは地に落ちる。
ここで投入すべきなのが `ReaderWriterLockSlim` である。
—
2. `ReaderWriterLockSlim` の核心概念
`ReaderWriterLockSlim` は、.NET Framework 3.5以降(および .NET Core / .NET 5以降)で導入された、軽量かつ高度なロック機構だ。以下の3つの状態を制御する。
1. 共有モード(Read Mode):
複数のスレッドが同時に読み取りを行うことを許可する。書き込みスレッドが待機していなければ、読み取りスレッドは無限に並行実行できる。
2. 排他モード(Write Mode):
1つのスレッドのみが書き込み(更新)を行える。このモードに入ると、他のすべての読み取りおよび書き込みスレッドはブロックされる。
3. アップグレード可能モード(Upgradeable Read Mode):
「データを読んでから、条件によって書き込みに昇格させたい」というシナリオで使う。デッドロックを防ぎつつ効率的にロックを切り替えるための高度なモードだ。
—
3. 【実装】極限まで最適化されたスレッドセーフ・キャッシュクラス
実際のエンタープライズ開発に耐えうる、`ReaderWriterLockSlim` をラップしたインメモリ・キャッシュの決定版コードを示す。
メモリリークを防ぐための `IDisposable` パターンの実装、および例外発生時確実にロックを解放する `Try〜Finally` の徹底に注目してほしい。
Imports System.Threading
Imports System.Collections.Generic
Namespace Enterprise.Threading.Cache
”’
”’
Public NotInheritable Class OptimizedMemoryCache(Of TKey, TValue)
Implements IDisposable
‘ 共有データストア
Private ReadOnly _cacheStore As New Dictionary(Of TKey, TValue)()
‘ 極限の並行処理を実現するReaderWriterLockSlim
‘ ※ リカバリ不能な例外を防ぐため、RecursionPolicy.NoRecursion(再帰なし)を推奨
Private ReadOnly _lock As New ReaderWriterLockSlim(LockRecursionPolicy.NoRecursion)
‘ 破棄フラグ
Private _disposed As Boolean = False
”’
”’
Public Function GetValue(key As TKey, ByRef value As TValue) As Boolean
‘ 読み取りロックを取得
_lock.EnterReadLock()
Try
Return _cacheStore.TryGetValue(key, value)
Finally
‘ 確実にロックを解放(例外発生時も必須)
_lock.ExitReadLock()
End Try
End Function
”’
”’
Public Sub SetValue(key As TKey, value As TValue)
‘ 書き込みロックを取得
_lock.EnterWriteLock()
Try
_cacheStore(key) = value
Finally
_lock.ExitWriteLock()
End Sub
End Sub
”’
”’
Public Sub ReplaceAll(NewData As Dictionary(Of TKey, TValue))
_lock.EnterWriteLock()
Try
_cacheStore.Clear()
For Each kvp In NewData
_cacheStore.Add(kvp.Key, kvp.Value)
Next
Finally
_lock.ExitWriteLock()
End Sub
End Sub
Region ” IDisposable Support ”
Public Sub Dispose() Implements IDisposable.Dispose
Dispose(True)
GC.SuppressFinalize(Me)
End Sub
Private Sub Dispose(disposing As Boolean)
If Not _disposed Then
If disposing Then
‘ マネージド資源の解放
If _lock IsNot Nothing Then
_lock.Dispose()
End If
If _cacheStore IsNot Nothing Then
_cacheStore.Clear()
End If
End If
_disposed = True
End If
End Sub
End Region
End Class
End Namespace
—
4. チーフアーキテクトが教える実装上の極意と罠
このコードを現場に投入する際、シニアエンジニアとして知っておくべき「Deepな知見」を共有する。
① `LockRecursionPolicy.NoRecursion` の原則
コンストラクタで指定している `LockRecursionPolicy.NoRecursion` は極めて重要だ。デフォルトの再帰許可モードにすると、同一スレッド内で誤って二重にロックを取得した際にパフォーマンスが大幅に劣化し、デッドロックの温床となる。モダンな設計では「再帰は悪」と割り切り、常に `NoRecursion` を強制すべきである。
② アップグレード可能ロックの誘惑と回避
「データが存在しなければ追加する(Check-Then-Act)」という処理で、`UpgradeableReadMode` を使いたくなる衝動に駆られる。しかし、安易なアップグレード可能ロックは、複数スレッドが同時にアップグレードを試みた際にデッドロックを引き起こすリスクがある。
基本的には、「読み取りは完全な読み取りロック」「書き込みは完全な書き込みロック」と割り切り、必要に応じて書き込みロックへ直接移行するか、あるいは楽観的並行性を採用する方が、バグの無い堅牢なシステムに仕上がる。
③ Windows API / アンマネージド資源との連携時の注意点
もしこのキャッシュの背後で、ネイティブDLL(Windows API)の呼び出しやCOMオブジェクトの操作を行う場合、ロック保持時間を極限まで短縮しなければならない。ロック内で重いI/OやAPI呼び出しを行うと、待機中のスレッドプールが枯渇し、プロセス全体がフリーズする(スレッドプール・スターベーション)。
—
5. まとめ
VB.NETは、レガシーなイメージ語られがちだが、.NETのランタイム(CLR)の進化をそのまま享受できる強力な言語である。
`SyncLock` の思考停止から脱却し、`ReaderWriterLockSlim` を適切な粒度で使いこなすことで、高負荷なエンタープライズ環境であっても、スループットを限界まで引き上げることが可能となる。
アーキテクトとしてコードを書くときは、常に「このロックの裏で何のスレッドがどれだけの頻度で待機しているか」を脳内ビジュアライゼーションできなければならない。本稿の知見が、あなたのシステムを次のステージへと引き上げる羅針盤となることを期待する。
