【実務・中級編】VB.NETにおけるShared(静的)メンバーの落とし穴:マルチスレッド環境での競合を防ぐスレッドセーフなクラス設計 – Visual Basic (VB / VB.NET)解析バイブル

スポンサーリンク

VB.NETにおけるShared(静的)メンバーの落とし穴:マルチスレッド環境での競合を防ぐスレッドセーフなクラス設計

業務自動化ツールやバックグラウンド処理を行う常駐アプリケーションをVB.NETで開発していると、一度は`Shared`キーワード(C#でいう`static`)の誘惑に駆られるはずだ。

「インスタンスをわざわざ生成しなくても呼び出せる」
「どこからでもグローバルにアクセスできて便利だ」

だが、開発現場のリーダーとしてハッキリ言おう。安易な`Shared`変数の多用は、マルチスレッド環境における百害あって一利なしの爆弾である。

今回は、複数スレッドからの同時アクセスが引き起こす不可解なバグの正体を暴き、現場で即座に使えるスレッドセーフなクラス設計の極意を伝授する。

1. なぜ `Shared` メンバーはマルチスレッドで危険なのか?

`Shared` メンバー(変数やメソッド)は、メモリ上にたった一つだけ実体が生成され、アプリケーション全体で共有される。

シングルスレッドで動く古いタイプのVBAマクロや単純なシーケンシャル処理であれば問題は起きない。しかし、近年の業務ツールは、UIの応答性を維持するための非同期処理(`Async` / `Await`)、複数ファイルの並行インポート、タイマーによるバックグラウンド実行など、無意識のうちにマルチスレッド(並行処理)で稼働している。

ここで何が起きるか?
複数のスレッドが同時に同じ `Shared` 変数を読み書きすると、「競合状態(Race Condition)」が発生する。

ありがちな「消えないバグ」のシナリオ

例えば、共有のロガーやカウンタークラスを作り、`Shared` 変数で処理件数をカウントアップしているとする。

‘ 【アンチパターン】絶対にやってはいけないShared変数のインクリメント
Public Class ProcessCounter
Public Shared CurrentCount As Integer = 0

Public Shared Sub Increment()
‘ この1行に見えて、内部では「読み取り」「加算」「書き込み」の3つのステップがある
CurrentCount += 1
End Sub
End Class

CPUの実行レベルで見ると、`CurrentCount += 1` は不可分(アトミック)な操作ではない。
1. スレッドAが `CurrentCount`(現在値:10)を読み込む。
2. スレッドBがほぼ同時に `CurrentCount`(現在値:10)を読み込む。
3. スレッドAが 10 + 1 = 11 を書き込む。
4. スレッドBが 10 + 1 = 11 を書き込む。

本来なら「12」になるべきカウンターが「11」になる。これがマルチスレッドの怖さだ。エラーメッセージすら出ず、データが静かに破損していく。

2. ファイル・データベース連携における致命傷

この問題が、テキストファイルへの書き込みやデータベース接続を伴う処理で発生すると、さらに深刻な事態を招く。

  • ファイルI/Oの競合: 複数のスレッドが同時に `Shared` なファイルライターを呼び出し、ログファイルにアクセスすると `IOException`(ファイルは別のプロセスによって使用されています)が即座に飛ぶ。
  • DBコネクションの共有: ADO.NETのコネクションやコマンドオブジェクトを `Shared` にすると、別スレッドのクエリによって接続状態が上書きされ、意図しないデータを操作したり、トランザクションが崩壊する。

業務自動化ツールにおいて「データが消えた」「ログが欠損した」というクレームは致命的である。これを防ぐには、排他制御(Synchronization)を完璧に施すか、そもそも不必要な `Shared` を排除する設計にシフトしなければならない。

3. 実装:`SyncLock` による堅牢な排他制御

どうしても `Shared` メンバーを共有資源として使わざるを得ない場合(キャッシュや設定値の保持など)、`SyncLock` ステートメントを使って「同時にアクセスできるスレッドを1つに制限(排他制御)」する。

以下のプロダクションコードは、スレッドセーフに安全に数値をインクリメント・取得するクラスの実装例だ。

Imports System.Threading

Public NotInheritable Class SafeCounter
‘ 外部からのインスタンス化を禁止(静的クラスとしての設計)
Private Sub New()
End Sub

‘ 共有されるデータ
Private Shared s_count As Integer = 0

‘ 排他制御用の専用ロックオブジェクト(外から触れないようにPrivate ReadOnlyにする)
Private Shared ReadOnly s_lockObj As New Object()

”’

”’ スレッドセーフなインクリメント
”’

Public Shared Sub Increment()
‘ Lockオブジェクトを用いて、このブロック内には同時に1スレッドしか入れないようにする
SyncLock s_lockObj
s_count += 1
End SyncLock
End Sub

”’

”’ スレッドセーフな値の取得
”’

Public Shared Function GetCount() As Integer
SyncLock s_lockObj
Return s_count
End SyncLock
End Function
End Class

設計のポイント

1. 専用のロックオブジェクトを用意する: `SyncLock Me` や `SyncLock GetType(SafeCounter)` はデッドロックや予期せぬロック競合の原因になるため、必ず専用のプライベートなインスタンス (`Private Shared ReadOnly s_lockObj As New Object()`) を用意せよ。
2. ロックの範囲は最小限にする: `SyncLock` の中身が長すぎると、マルチスレッドのメリットである「並行処理」の意味が薄れ、アプリケーション全体がノロノロ運転になる(ボトルネックの発生)。

4. プロが選ぶ最高のアプローチ:インスタンスベースへの転換

`SyncLock` は強力だが、ロックの掛け忘れやデッドロックのリスクが常に伴う。

真にモダンで保守性の高いVB.NET設計において、最も推奨されるアプローチは、「そもそも `Shared` 変数をなくし、インスタンスごとに状態を持たせる(依存性注入 / DI の思想)」ことだ。

もしどうしても「アプリケーション全体で1つの状態を安全に管理したい」のであれば、`Shared` 変数を剥き出しにするのではなく、[.NETの `Lazy(Of T)` クラス](https://learn.microsoft.com/ja-jp/dotnet/framework/performance/lazy-initialization)や、Thread-Safeなコレクション(`ConcurrentDictionary` など)を活用すべきだ。

まとめ:現場で迷ったらどうすべきか?

1. 安易に `Shared` 変数(特に書き込み可能な変数)を作るな。
2. 状態を持たない純粋なユーティリティメソッド(文字列操作や計算など)以外で `Shared` を使う場合は、マルチスレッドからのアクセスを前提に疑え。
3. 共有資源へのアクセスが必要な場合は、必ず `SyncLock` で排他制御をかけるか、インスタンスを都度生成する設計へリファクタリングせよ。

「動けばいいや」で書いた数行の `Shared` が、数ヶ月後に現場で再現性のない怪奇現象を引き起こす。
プロのエンジニアであれば、コードの裏側で何が起きているのか(メモリとスレッドの挙動)を常に意識した、堅牢な設計を心がけてほしい。

タイトルとURLをコピーしました