Windows Formsの呪縛を解く:排他制御の「本質」と「実戦的アーキテクチャ」
現場のエンジニアからよく聞く悲鳴がある。「ユーザーが同時に画面を開いてデータを更新したせいで、データが壊れた」「二重起動防止が機能せず、設定ファイルがロックされた」。
これらは単なるバグではない。「排他制御のレイヤー」を履き違えた設計の敗北だ。
Windows Formsはイベント駆動という性質上、非同期的な操作に無防備だ。今回は、データ整合性を守りつつ、現場を止めないための「排他制御の極意」を授ける。
—
1. なぜ「楽観的排他制御」が正義なのか
データベースの排他には、悲観的と楽観的がある。しかし、業務アプリにおいて「編集中にずっとDBの行をロックする(悲観的)」という設計は悪手だ。ユーザーが席を外せば、システム全体が麻痺するからだ。
楽観的排他制御(Optimistic Locking)の基本は、「更新直前までロックしない」ことにある。
実装の要:バージョンカラム
テーブルに `RowVersion`(または `UpdateTime`)カラムを持たせ、更新クエリでこれを確認する。
‘ 楽観的排他制御:更新の瞬間、条件に一致しなければ例外を投げる
Public Sub UpdateData(id As Integer, newName As String, currentVersion As Integer)
Using conn As New SqlConnection(“接続文字列”)
conn.Open()
Dim sql As String = “UPDATE M_Master SET Name = @Name, Version = Version + 1 ” &
“WHERE Id = @Id AND Version = @Version”
Using cmd As New SqlCommand(sql, conn)
cmd.Parameters.AddWithValue(“@Name”, newName)
cmd.Parameters.AddWithValue(“@Id”, id)
cmd.Parameters.AddWithValue(“@Version”, currentVersion)
Dim affectedRows As Integer = cmd.ExecuteNonQuery()
If affectedRows = 0 Then
‘ ここで例外を投げるか、ユーザーに通知する
Throw New DBConcurrencyException(“他ユーザーによってデータが更新されました。画面を再読み込みしてください。”)
End If
End Using
End Using
End Sub
この「Versionを条件に入れて更新行数を確認する」という一手間が、システムを堅牢にする。
—
2. アプリ間排他:Mutexによる多重起動の封殺
次に、ローカルの設定ファイルやログを複数のアプリインスタンスが同時に触るリスクだ。VB.NETでは `System.Threading.Mutex` を使うのが定石だが、「名前付きミューテックス」のスコープを理解していないコードが多すぎる。
‘ Program.vb または起動時のエントリポイントで実装
Private Shared mutex As System.Threading.Mutex
Shared Sub Main()
Dim createdNew As Boolean
‘ グローバルな名前空間を意識した一意のID
mutex = New System.Threading.Mutex(True, “Global\MyCompany_MyApp_UniqueId”, createdNew)
If Not createdNew Then
MessageBox.Show(“既にアプリケーションが起動しています。”, “二重起動エラー”, MessageBoxButtons.OK, MessageBoxIcon.Error)
Return
End If
Application.Run(New MainForm())
‘ プロセス終了時に解放
mutex.ReleaseMutex()
End Sub
「Global\」を付けることで、ターミナルサーバー等のセッションを超えた排他が可能になる。これを見落とすと、現場で「二重起動した!」という報告が止まらなくなるぞ。
—
3. ファイルロックの罠と「リトライ戦略」
DBではなく設定ファイルやCSVを直接読み書きする場合、`FileStream` を使うのが鉄則だ。`File.Open` は甘えである。
共有違反を防ぐには、ファイルを開く際に「共有モード」を厳密に制御すること。
Public Sub SafeWriteFile(path As String, content As String)
Dim retryCount As Integer = 3
While retryCount > 0
Try
‘ 読み取り専用で共有し、書き込み時はロックする
Using fs As New FileStream(path, FileMode.OpenOrCreate, FileAccess.Write, FileShare.None)
Using sw As New StreamWriter(fs)
sw.Write(content)
End Using
End Using
Return
Catch ex As IOException
‘ 誰かが掴んでいる場合は少し待機してリトライ
System.Threading.Thread.Sleep(500)
retryCount -= 1
End Try
End While
Throw New Exception(“ファイルへの書き込みに失敗しました。他のプロセスが使用中か確認してください。”)
End Sub
極意: ファイル操作は「失敗するもの」という前提でリトライ処理を組み込むこと。ネットワークドライブ上のファイル操作であれば尚更だ。
—
現場のリーダーから最後に
いいか、排他制御とは「ユーザーにストレスを与えないための技術」だ。
1. DBは楽観的排他で軽快に保て。
2. 多重起動はMutexで根絶しろ。
3. ファイル操作は失敗を見越したリトライを入れろ。
この3つを守るだけで、君の書くコードの信頼性は劇的に向上する。小手先のテクニックではなく、データとプロセスのライフサイクルを意識した設計こそが、プロフェッショナルの仕事だ。
さあ、IDEを開け。妥協のないコードを書いてくれ。
