【実務・中級編】実務中級者向け:VB.NETでの「System.IO.FileStream」と「FileShare」を活用した排他制御:別プロセスでロックされたファイルの安全な読み書き手法 – Visual Basic (VB / VB.NET)解析バイブル

スポンサーリンク

業務効率化ツールの開発において、初心者が最初にぶつかり、かつベテランでも設計を誤ると致命的なバグを誘発するのが「ファイル共有の競合」だ。

「さっきまで動いていたツールが、ユーザーがExcelでファイルを開いた瞬間に例外を吐いて止まった」
「ログを出力しようとしたら、別プロセスの書き込みと衝突してデータが壊れた」

もし君が `File.ReadAllLines` や `My.Computer.FileSystem.ReadAllText` といった「お手軽関数」だけで戦っているのなら、それは戦場に丸腰で挑むようなものだ。これらは内部でファイルロックの制御をブラックボックス化しており、実務で求められる「柔軟かつ堅牢なファイル操作」には耐えられない。

今回は、VB.NETにおけるファイル制御の真髄、`System.IO.FileStream` と `FileShare` 列挙体を駆使した、プロフェッショナルな排他制御のレシピを伝授する。

1. なぜ君のコードは「プロセス競合」で落ちるのか

まず、OS(Windows)がファイルをどう扱っているかを理解せよ。
Windowsにおいて、ファイルを開くという行為は「ハンドル(取っ手)」を掴む行為だ。このとき、後から来た奴に「どこまでの操作を許すか」を宣言する必要がある。

多くの簡易メソッドは、内部的に `FileShare.Read`(自分以外は読み取りしか許さない)や `FileShare.None`(自分以外には何もさせない)を選択している。そのため、Excelが「上書き保存するためにロック」しているファイルにアクセスしようとすると、OSが「拒絶」を返し、君のアプリは例外で即死するのだ。

実務で必要なのは、相手のロック状態を察知し、「隙間から安全に覗き見させてもらう」あるいは「書き込みが終わるまで紳士的に待機する」設計である。

2. 核心:FileShare.ReadWrite という「平和条約」

競合を回避する最大の武器は、`FileStream` コンストラクタの第4引数に渡す `FileShare` 列挙体だ。

特に `FileShare.ReadWrite` は強力だ。これは「私がファイルを開いている間、他の誰かが読み込んでも、書き込んでも構いませんよ」という宣言だ。相手(例えばExcelやログ出力プロセス)も同様の寛容な設定で開いていれば、たとえ相手が編集中であっても、最新の状態を読み取ることが可能になる。

3. 【実践】堅牢なファイル読み込み用ラッパー関数

コピペで動作し、かつ商用環境に耐えうる「リトライ機能付き」の読み込み関数を紹介する。
単に開くだけでなく、「アンマネージドリソースの確実な解放」「一時的なロックに対するリトライアルゴリズム」を組み込んでいる。

.net
Imports System.IO
Imports System.Text
Imports System.Threading

”’

”’ ファイル競合を考慮した堅牢なテキスト読み込みユーティリティ
”’

Public Class RobustFileHandler

”’

”’ 他のプロセスが使用中のファイルでも、可能な限り共有して読み込む
”’

”’ 対象ファイルパス ”’ 文字エンコーディング ”’ リトライ回数 ”’ リトライ間隔(ms) ”’ ファイル内容の文字列
Public Shared Function SafeReadAllText(filePath As String,
Optional encoding As Encoding = Nothing,
Optional maxRetries As Integer = 5,
Optional delayMilliseconds As Integer = 500) As String

If Not File.Exists(filePath) Then Throw New FileNotFoundException(“対象ファイルが存在しません。”, filePath)
If encoding Is Nothing Then encoding = Encoding.UTF8

Dim lastException As Exception = Nothing

‘ プロフェッショナルの設計:一時的なロックに備えたリトライループ
For i As Integer = 1 To maxRetries
Try
‘ FileStreamを直接生成し、FileShare.ReadWriteを明示
‘ これにより、他プロセスが書き込み中でも読み取りを試行する
Using fs As New FileStream(filePath,
FileMode.Open,
FileAccess.Read,
FileShare.ReadWrite Or FileShare.Delete)

Using sr As New StreamReader(fs, encoding)
Return sr.ReadToEnd()
End Using
End Using

Catch ex As IOException
‘ 32:プロセスがファイルにアクセスできない(他が排他ロック中)
‘ 33:ファイルの一部がロックされている
Dim errorCode As Integer = System.Runtime.InteropServices.Marshal.GetHRForException(ex) And &HFFFF

If errorCode = 32 OrElse errorCode = 33 Then
lastException = ex
‘ 指数関数的バックオフ(徐々に待ち時間を増やす)も検討に値するが、
‘ 業務アプリなら一定間隔のリトライで十分なケースが多い
Thread.Sleep(delayMilliseconds)
Else
Throw ‘ 共有違反以外の致命的なIOエラーは即座にスロー
End If
End Try
Next

Throw New IOException($”ファイル ‘{filePath}’ は他プロセスによりロックされており、{maxRetries}回のリトライ後もアクセスできませんでした。”, lastException)
End Function

End Class

このコードの「重み」を理解せよ

1. `FileShare.ReadWrite Or FileShare.Delete`:
単に `ReadWrite` だけでなく `Delete` も加えているのは、モダンなアプリがファイルを一時的にリネームしたり削除したりする挙動(アトミックな更新)を妨げないためだ。
2. `Marshal.GetHRForException`:
`Try-Catch` で `IOException` を捕まえるだけでは不十分だ。ディスクフルなのか、パスが長いのか、それとも本当に「共有違反」なのか。Win32エラーコード(32や33)をチェックすることで、「待てば解決する問題」か「即座に諦めるべき問題」かを峻別している。
3. `Using` のネスト:
`FileStream` はOSのハンドルを直接消費する。`Using` を使わずに放置することは、メモリリークならぬ「ハンドルリーク」を引き起こし、システム全体の不安定化を招く。これはアーキテクトとして絶対に許されない。

4. データベース連携やログ出力への応用

読み込みだけでなく、「書き込み」でもこの知識は必須だ。
例えば、共有フォルダ上のCSVをDBのように扱う場合、複数のクライアントが同時に `FileShare.ReadWrite` で開いて書き込むと、データが混ざり合う「ダーティ・ライト」が発生する。

その場合は、以下のような戦略を使い分けろ。

  • 追記型ログの場合: `FileShare.Read` を指定して開き、他者の読み取りは許可するが、書き込みは自分一人に制限する。
  • マスターデータ更新の場合: 一時ファイルに書き込んでから `File.Replace` で置換する。この際も `FileShare` の制御が必要だ。

5. チーフアーキテクトからの助言

「動けばいい」というコードは、平時には牙を剥かない。しかし、システムの負荷が上がり、複数のプロセスがミリ秒単位でファイルを取り合う極限状態において、その真価――あるいは脆弱性――が露呈する。

`System.IO` を制する者は、Windows OSの挙動を制する。
簡易メソッドの裏側にある `FileStream` の挙動を掌握し、`FileShare` という「交渉術」を身につけること。それが、現場で信頼される堅牢な自動化ツールを生むための第一歩である。

君のコードが、ユーザーの「ファイルを開きっぱなし」という些細な不注意で止まらない、真にプロフェッショナルなものになることを期待している。

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