業務自動化の最前線で戦う諸君なら、一度は「System.IO.IOException: プロセスはファイルにアクセスできません。別のプロセスが使用中です。」という絶望的なエラーメッセージに直面したことがあるはずだ。
特に、レガシーな基幹システムが吐き出す巨大なCSV、誰かが開きっぱなしにしている共有サーバー上のExcel、あるいはリアルタイムで更新され続けるログファイル。これらを「安全に、かつ確実に」読み取ることは、単なるコーディングスキルを超えた、Windows OSのファイルシステムとリソース管理に対する深い洞察が求められる領域である。
今回は、VB.NETにおける`System.IO.FileStream`と`FileShare`列挙体を使いこなし、他プロセスの占有をすり抜けながらデータの整合性を保つ「極限のファイル操作」について解説する。
—
1. なぜ「File.ReadAllLines」は現場で通用しないのか
初心者は往々にして `File.ReadAllLines` や `File.ReadAllText` を多用する。しかし、これらのメソッドは内部的にファイルをオープンする際、共有モードを厳格に制限する。
もし対象のファイルが他のアプリケーション(例えばExcelや、追記中のロガー)によって書き込みロックされている場合、これらのメソッドは即座に例外をスローして停止する。業務自動化において「相手がファイルを閉じるまで待つ」という設計は、デッドロックやタイムアウトの温床であり、三流の仕事と言わざるを得ない。
我々プロフェッショナルが手に取るべきは、`System.IO.FileStream`による低レイヤーな制御である。
—
2. FileShare列挙体:OSへの「共有の宣言」
`FileStream`をインスタンス化する際、最も重要な引数が `FileShare` だ。これは、自分がファイルを開いている間に「他者がそのファイルに対して何をすることを許すか」をWindowsカーネル(CreateFile API)に伝えるためのフラグである。
- FileShare.None: 他のプロセスはいかなるアクセスもできない。完全な独占。
- FileShare.Read: 他のプロセスは読み取りのみ可能。
- FileShare.Write: 他のプロセスは書き込みのみ可能。
- FileShare.ReadWrite: 他のプロセスが読み書き両方を行うことを許可する。
「別プロセスが書き込み中のログを読み取る」というミッションにおいて、正解は `FileShare.ReadWrite` を指定することだ。これにより、相手が書き込みのためにファイルを開いていても、OSは「この読み取りプロセスは、他者が書き込むことを許容している」と判断し、ハンドルを貸し出してくれる。
—
3. 実践:ロックを回避する堅牢な読み取り実装
以下に、実務でそのまま転用できる「リトライロジック付き・共有モード読み取り」のテンプレートを示す。
.net
Imports System.IO
Imports System.Text
Imports System.Threading
”’
”’
Public Class FileAccessArchitect
”’
”’
”’ 対象ファイルのパス
”’
Public Function ForceReadLockedFile(filePath As String) As String
‘ リトライ回数と待機時間は業務要件に合わせて調整せよ
Const MaxRetries As Integer = 5
Const DelayMilliseconds As Integer = 1000
Dim lastException As Exception = Nothing
For i As Integer = 1 To MaxRetries
Try
‘ FileStreamを明示的に生成。
‘ FileMode.Open: 既存ファイルを開く
‘ FileAccess.Read: 自身の権限は読み取りのみ
‘ FileShare.ReadWrite: 他プロセスの読み書きを一切制限しない(重要)
Using fs As New FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)
Using reader As New StreamReader(fs, Encoding.GetEncoding(“Shift_JIS”))
Return reader.ReadToEnd()
End Using
End Using
Catch ex As IOException
‘ ファイルが完全にロック(FileShare.Noneで開かれている等)されている場合
lastException = ex
Debug.Print($”Attempt {i}: ファイルがロックされています。再試行中…”)
Thread.Sleep(DelayMilliseconds)
End Try
Next
Throw New Exception($”ファイル {filePath} の読み取りに失敗しました。プロセスが完全に占有されています。”, lastException)
End Function
End Class
このコードの急所
1. Usingブロックの徹底: `FileStream` はアンマネージドリソース(ファイルハンドル)を保持する。`Using` を抜けた瞬間に `Dispose` が呼ばれ、OSにハンドルを返却することを保証しなければならない。これを怠る者は、自らが「ファイルをロックする元凶」となる。
2. FileShare.ReadWrite: これが今回の肝だ。たとえ他プロセスが `FileShare.Read` で開いていても、こちらが `ReadWrite` を許容していれば衝突は回避できる。
3. Encodingの明示: 日本のレガシーシステムは未だに `Shift_JIS` (CP932) の世界だ。デフォルトの `UTF-8` で読み込むという過ちは、データ破壊と同義である。
—
4. Windows API `CreateFile` の影を意識せよ
.NETの `FileStream` は、最終的に `kernel32.dll` の `CreateFile` 関数を呼び出している。Windowsのファイルシステムレベルでは、ファイルを開く際に「アクセス権(Access Mask)」と「共有モード(Share Mode)」の論理積が計算される。
もし、先行するプロセス(例えば一部の行儀の悪い古いツール)が `dwShareMode = 0` (FileShare.None) でファイルを開いている場合、後続の我々がどれほど `FileShare.ReadWrite` を叫んだところで、OSはアクセスを拒否する。
その場合、唯一の対抗策は「リトライ」である。相手がハンドルを放す一瞬の隙を突く。上記のコードにリトライロジックを組み込んでいるのは、それが単なる理想論ではなく、泥臭い現場の真実だからだ。
—
5. メモリ最適化とパフォーマンスの極致
巨大なログファイルを扱う場合、`ReadToEnd()` は避けるべきだ。メモリ(Managed Heap)を一度に数百MB消費し、GC(Garbage Collector)に過度な負荷をかける。
.net
‘ 巨大ファイルはバッファリングしながら一行ずつ処理する
Using fs As New FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)
Using reader As New StreamReader(fs, Encoding.GetEncoding(“Shift_JIS”))
While Not reader.EndOfStream
Dim line As String = reader.ReadLine()
‘ ここで一行ずつ処理。メモリ消費を一定に保つ。
End While
End Using
End Using
この「一行ずつ(Stream-based)」の処理こそが、エンタープライズ級のバッチ処理において求められる「枯れた技術」の真髄である。
—
結論:アーキテクトとしての矜持
VB.NETという言語は、しばしば初心者向けと揶揄される。しかし、その背後にある.NET FrameworkおよびWindows OSの機構を理解し、`FileStream` の一引数にまで魂を込めることができれば、これほど強力な武器はない。
「ファイルが開けない」という些細なトラブルで止まらない、強靭な自動化システム。それを作るのは、言語の仕様書をなぞるだけのプログラマではなく、OSと対話し、リソースのライフサイクルを完全に掌握するチーフアーキテクトである諸君だ。
このレシピを自身のライブラリに加え、現場の「動かない」を「止まらない」に変えてほしい。
