更新履歴(3件・最終更新 2026年08月02日)
この記事に加えた変更の記録です。アーカイブした更新前のバージョンは、DOI付きの固定URLから読めます。
- 記事の冒頭に「この記事の知識マップ」節を追加しました。本文で扱っている概念とその関係を、要約・図・詳細ページへのリンクにまとめたものです。本文の主張は変えていません。
- 外部レビュー(1283件)への対応として本文を更新しました。個々の変更内容は、この下の履歴を参照してください。
- 用語を先にそろえる節を新設し、アンチパターンと対策の対応表を追加しました。共有フォルダ越しの注意4点(共有をまたぐrenameは原子的でないこと、他プロセスが開いているだけで失敗すること、タイムスタンプの保証がハンドルを閉じたときだけであること、変更通知の取りこぼし)と、バイト範囲ロックがメモリマップでは無視されることの出典を追加しました。
- 初版公開
この記事を引用する(DOI: 10.5281/zenodo.21589591)
この記事はZenodoにアーカイブされています。常に最新版へ解決されるDOIと、いま表示している版に固定されたDOIの両方を下に示します。
小村 豪(2026)「ファイル連携の排他制御の基礎知識 - ファイルロックと原子的 claim のベストプラクティス」合同会社小村ソフト. https://doi.org/10.5281/zenodo.21589591 https://staging.comcomponent.com/blog/2026/03/07/001-file-integration-locking-best-practices-komurasoft-style/
- DOI(最新版)
- 10.5281/zenodo.21589591
- DOI(この版)
- 10.5281/zenodo.21732614
ファイル連携の排他制御は、共有フォルダや夜間バッチ、別プロセス連携でほぼ必ず問題になります。 特に検索で多いのは、ファイルロックだけで十分なのか、複数ワーカーが同じファイルを拾わない方法は何か、途中書き込みのファイルをどう避けるか、といった悩みです。
この記事では、ファイルロック、原子的 claim、temp -> rename、idempotency を軸に、ファイル連携の排他制御を見ていきます。
用語を先にそろえる
この分野は、英語のまま定着している言葉が多く、意味がふわっとしたままだと読みづらくなります。先にこの記事での意味を固定しておきます。
| 用語 | この記事での意味 |
|---|---|
| 原子的(atomic) | 途中の状態が他から見えない操作のことです。成功したか、何も起きなかったかのどちらかにしかなりません |
| claim | 「このファイルは自分が処理する」と処理権を確保することです。この記事では主に、incoming から processing/<worker>/ へ rename できた側だけが所有者になる形を指します |
| 原子的 claim | その claim を 1 操作で行うことです。確認と確保が分かれていると、その隙間に他プロセスが入れます(3.1) |
| lease | 有効期限付きの所有権です。lock file に「誰が」「いつまで」を書いておき、期限が切れたら他のワーカーが引き取れるようにします(4.4) |
| stale | 持ち主が異常終了したのに残っている lock や claim の状態です。生きているのか死んでいるのか判定できないと、全員が止まります(2.3) |
| manifest | 本体ファイルとは別に置く、内容の説明ファイルです。ファイル名、サイズ、ハッシュ、レコード数などを書き、受信側の検証に使います。done ファイルはその最小版です(4.2) |
| idempotency(冪等性) | 同じ入力をもう一度処理しても、結果が変わらない性質のことです(4.5) |
| advisory lock | 参加者全員がその約束を守る前提でだけ効くロックです。OS が強制しないので、無視して読み書きするプログラムも普通に書けます。Linux の flock がこの型です |
| byte-range lock | ファイル全体ではなく、指定した範囲だけを対象にするロックです。Windows の LockFileEx が代表で、こちらは OS が強制します。ただし例外があります(3.5) |
この記事の知識マップ
この記事は、共有フォルダや夜間バッチでのファイル連携を、OSロック任せではなく受け渡しプロトコルとして設計する考え方を整理する。読む前に処理権を原子的claimで確保し、生成中のファイルはtemp名に閉じ込めてcloseしたあとfinal名へrenameして公開し、完了はサイズやタイムスタンプで推測せずdone・manifestで明示する。lock fileを使うならownerIdやexpiresAtを持つlease形式にしてstale lockに備え、Windowsのbyte-range lockがメモリマップファイルでは無視されることやadvisory lockが約束を無視する相手には効かないことを踏まえたうえで、最後は同じ入力を再処理しても壊れないidempotencyで受け止める設計が実務では強いと結論づける。
flowchart LR
accTitle: ファイル連携の排他制御の知識マップ
accDescr: 受け渡しプロトコルが原子的claim・temp->rename公開・done/manifest・lease化したlock file・idempotencyをどう組み合わせ、二重処理や書き込み途中の読み込みなどの事故をどのアンチパターンに対応させて防ぐかを示す図
file_handoff_protocol["受け渡しプロトコル"]
atomic_claim["原子的claim"]
temp_then_rename_publish["temp -> close -> rename/replaceでの公開"]
done_manifest_file["done/manifestファイル"]
lock_file_lease["lease化したlock file"]
idempotent_processing["idempotency(冪等性)を前提にした処理"]
os_file_lock["OSファイルロック(OSロック)"]
atomic_creation["原子的作成(CreateNew / O_CREAT|O_EXCL)"]
duplicate_processing["二重処理(二重計上・二重送信・更新の消失)"]
exists_then_create_antipattern["Exists->Createの二段階チェック"]
direct_final_write_antipattern["最終ファイル名への直接書き込み"]
partial_write_read["書き込み途中ファイルの読み込み事故"]
size_stability_completion_check["ファイルサイズ安定待ちによる完了判定"]
shared_file_mutual_update_antipattern["共有ファイルの相互更新アンチパターン"]
stale_lock["stale lock"]
byte_range_lock["byte-range lock(範囲ロック)"]
heterogeneous_system_integration["異種システム連携"]
advisory_lock["advisory lock"]
cross_volume_rename_fallback["クロスボリュームのrenameフォールバック(コピー+削除)"]
smb_share_file_handoff["共有フォルダ(SMB)越しのファイル連携"]
rename_fails_on_open_handle["renameが開いているだけで失敗する"]
file_timestamp_unreliability["ファイルタイムスタンプの不確実性"]
periodic_directory_listing["定期的なディレクトリ列挙"]
change_notification_loss["変更通知の取りこぼし"]
file_handoff_protocol -->|"利用する"| atomic_claim
file_handoff_protocol -->|"利用する"| temp_then_rename_publish
file_handoff_protocol -->|"利用する"| done_manifest_file
file_handoff_protocol -->|"利用する"| lock_file_lease
file_handoff_protocol -->|"利用する"| idempotent_processing
file_handoff_protocol -.->|"利用する"| os_file_lock
atomic_claim -.->|"利用する"| atomic_creation
atomic_claim -->|"防止する"| duplicate_processing
exists_then_create_antipattern -->|"原因になり得る"| duplicate_processing
atomic_claim -->|"推奨される対応"| exists_then_create_antipattern
atomic_creation -->|"推奨される対応"| exists_then_create_antipattern
direct_final_write_antipattern -->|"原因になり得る"| partial_write_read
temp_then_rename_publish -->|"防止する"| partial_write_read
size_stability_completion_check -.->|"原因になり得る"| partial_write_read
done_manifest_file -->|"推奨される対応"| size_stability_completion_check
shared_file_mutual_update_antipattern -->|"原因になり得る"| duplicate_processing
idempotent_processing -->|"推奨される対応"| duplicate_processing
lock_file_lease -->|"推奨される対応"| stale_lock
lock_file_lease -->|"前提とする"| atomic_creation
os_file_lock -->|"利用する"| byte_range_lock
os_file_lock -.->|"軽減する"| duplicate_processing
os_file_lock -->|"用いるのは非推奨"| heterogeneous_system_integration
advisory_lock -.->|"用いるのは非推奨"| heterogeneous_system_integration
temp_then_rename_publish -.->|"両立しない"| cross_volume_rename_fallback
smb_share_file_handoff -.->|"原因になり得る"| cross_volume_rename_fallback
smb_share_file_handoff -.->|"原因になり得る"| rename_fails_on_open_handle
done_manifest_file -->|"推奨される対応"| file_timestamp_unreliability
periodic_directory_listing -->|"推奨される対応"| change_notification_loss
smb_share_file_handoff -.->|"原因になり得る"| change_notification_loss
図の実線は常に成り立つ関係、破線は条件付きの関係です(成立条件は詳細ページの各関係の説明に記載)。関係すべての一覧(全29件、根拠・確度つき)と主要概念の定義は知識マップ詳細ページにまとめています。データ: JSON-LD / Turtle
目次
- まず結論(ひとことで)
- ファイル連携で起きる競合パターン(図)
- 2.1. 書き込み途中のファイルを読んでしまう
- 2.2. 複数ワーカーが同じファイルを同時に拾う
- 2.3. stale lock で全員が止まる
- アンチパターン
- 3.1.
Exists -> Createの二段階チェック - 3.2. 最終ファイル名へ直接書く
- 3.3. ファイルサイズが止まったら完了扱い
- 3.4. 共有ファイルをみんなで更新する
- 3.5. ロックAPIを万能と思う
- 3.1.
- ベストプラクティス
- 4.1.
temp -> close -> rename / replaceで公開する - 4.2.
done/ manifest で完全性を明示する - 4.3. 受信側は claim を原子的に取る
- 4.4. lock file に頼るなら lease にする
- 4.5. idempotency を前提にする
- 4.1.
- 擬似コード(抜粋)
- ざっくり使い分け
- まとめ
- 参考資料
ファイル連携は、コードそのものより「受け渡しの約束」のほうが壊れやすい分野です。 単体試験では通るのに、本番の共有フォルダや夜間バッチでだけたまに壊れる。しかも再現しづらい。わりと普通にあります。
原因の多くは、ファイルI/OのAPIそのものより、次の3つが曖昧なことです。
- いつ読んでよいのか
- 誰が処理権を持つのか
- 失敗したときにどう回復するのか
この記事では、ファイル連携の排他制御を OS ロックの話だけで終わらせず、受け渡しプロトコルとして整理します。
なお、この記事に登場するコードは、ビルド・実行できるサンプル一式(ライブラリ、2 ワーカーの claim 競合や lease 引き取りを実演するデモ、競合・破損・stale lock を再現するユニットテスト)として GitHub で公開しています。
file-integration-locking-best-practices-komurasoft-style - komurasoft-blog-samples (GitHub)
1. まず結論(ひとことで)
- ファイル連携で一番大事なのは、最終ファイル名が見えた時点で「もう読んでよい」状態を作ること
- 生成中 / 公開済み / 処理中 / 処理済み を、ファイル名やディレクトリで分けて表現すること
- 複数ワーカーがいるなら、読む前に claim を原子的に取ること
- lock file や OS ロックは補助として使い、最後は idempotency で受け止めること
要するに、ファイル連携では 排他制御 というより 受け渡しプロトコル の設計が本体です。 ロック関数を1つ呼べば終わり、とはなりません。
2. ファイル連携で起きる競合パターン(図)
2.1. 書き込み途中のファイルを読んでしまう
最終ファイル名に直接書き始めると、この事故が起きます。 JSON なら閉じ括弧がなく、CSV なら行数が足りず、ZIP なら普通に壊れます。
sequenceDiagram
participant 送信 as 送信側
participant 共有 as 共有フォルダ
participant 受信 as 受信側
送信->>共有: orders.csv を最終名で作成
送信->>共有: 1行目〜5000行目を書き込み中
受信->>共有: orders.csv を検知
受信->>共有: そのまま読み始める
Note over 受信: まだ途中
送信->>共有: 残りを書き込む
Note over 受信: 行数不足 / 解析失敗 / 一部だけ処理
2.2. 複数ワーカーが同じファイルを同時に拾う
「一覧を見て、未処理なら開く」という流れだと、同じファイルを2つのワーカーが掴めます。 二重計上や二重送信の始まりです。
sequenceDiagram
participant W1 as ワーカー1
participant W2 as ワーカー2
participant Dir as incoming
W1->>Dir: a.csv を見つける
W2->>Dir: a.csv を見つける
W1->>Dir: 読み込み開始
W2->>Dir: 読み込み開始
Note over W1,W2: 同じ入力を二重処理
2.3. stale lock で全員が止まる
lock file を置くだけの設計は、異常終了時に詰まりやすいです。 誰の lock か、まだ生きているのか、いつまで有効かが分からないと、後続が永遠に待つことになります。
sequenceDiagram
participant A as ワーカーA
participant Lock as lock ファイル
participant B as ワーカーB
A->>Lock: lock を作成
Note over A: ここで異常終了
B->>Lock: lock の存在を確認
B->>Lock: 処理開始を見送る
B->>Lock: さらに待つ
Note over B,Lock: stale か判定できず全員停止
3. アンチパターン
3.1. Exists -> Create の二段階チェック
これは、「確認」と「確保」が別操作 になっているのが問題です。 間に他プロセスが割り込めるので、排他になりません。
sequenceDiagram
participant A as プロセスA
participant B as プロセスB
participant FS as ファイルシステム
A->>FS: lock が無いか確認
B->>FS: lock が無いか確認
FS-->>A: 無い
FS-->>B: 無い
A->>FS: lock を作成
B->>FS: lock を作成
Note over A,B: 両方が進めてしまう
典型的な悪い例は、こういう形です。
if (!File.Exists(lockPath))
{
File.WriteAllText(lockPath, Environment.ProcessId.ToString());
ProcessFile();
}
必要なのは、「無ければ作る」を 1操作にすること です。
.NET なら FileMode.CreateNew 系、POSIX 系なら O_CREAT | O_EXCL のような原子的作成を使います。
3.2. 最終ファイル名へ直接書く
受信側が「その名前が見えたら読んでよい」と解釈しているなら、最終ファイル名に直接書き始めた時点で負けです。 見えること と 読んでよいこと を同じにしないのが基本です。
flowchart LR
A[final 名が見える] --> B[受信側が検知]
B --> C[送信側はまだ書き込み中]
C --> D[不完全なデータを読む]
using var writer = OpenForWrite(finalPath); // ここで finalPath が見えてしまう
foreach (var row in rows)
{
writer.WriteLine(row);
}
このやり方は、2.1 の事故を自分から呼び込みます。
3.3. ファイルサイズが止まったら完了扱い
これは便利そうに見えますが、かなり危ういです。 ネットワーク越しのコピー、送信側の一時停止、バッファリング、リトライで普通に揺れます。
sequenceDiagram
participant 送信 as 送信側
participant 共有 as 共有フォルダ
participant 受信 as 受信側
送信->>共有: data.zip をコピー開始
送信->>共有: 途中で一時停止
受信->>共有: サイズが10秒変わらない
Note over 受信: 完了と誤判定
受信->>共有: 読み込み開始
送信->>共有: コピー再開
if (currentLength == lastLength && stableSeconds >= 10)
{
return Ready;
}
完了を 推測 で決めると、共有フォルダや大きなファイルで足をすくわれます。 完了は manifest や done file で 明示 した方が安定します。
3.4. 共有ファイルをみんなで更新する
1つの status.csv や counter.json をみんなで読んで更新する設計は、だいたい最後に書いた人が勝ちます。
ファイル連携を簡易DBとして使い始めると、ここで苦しくなります。
sequenceDiagram
participant A as バッチA
participant B as バッチB
participant F as status.csv
A->>F: v1 を読む
B->>F: v1 を読む
A->>F: v2-A を書く
B->>F: v2-B を書く
Note over F: A の更新が消える
append-only に逃げる案もありますが、ファイルシステムや配置形態で意味が揺れます。 共有更新が必要なら、ここはファイル連携で無理をしない方がよいです。
3.5. ロックAPIを万能と思う
ロックAPIは重要ですが、全参加者が同じ約束で動く ときだけ効きます。 異種システム連携では、ここを過信しない方が安全です。
補足:
- Linux の
flockは advisory lock なので、約束を無視する相手は普通に書けます - Windows の byte-range lock は、メモリマップファイルでは無視されます
- つまり、OS ロック 単体で完了通知や所有権の設計まで背負わせない方がよいです
2 つ目は Windows の仕様として明記されています。Microsoft Learn の Locking and Unlocking Byte Ranges in Files には、他プロセスがロック済みの範囲へアクセスすると必ず失敗する(つまり Windows の範囲ロックは advisory ではなく強制される)と書かれた直後に、メモリマップファイルを使う場合には byte-range lock は無視される、という注意が置かれています。相手が CreateFileMapping 経由で同じファイルを触っていれば、こちらのロックは素通りされるということです。
.NET で範囲ロックを取るなら、FileStream.Lock / Unlock です(Windows の場合)。
using var stream = new FileStream(
path, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite);
// 先頭 1 バイトだけを「処理中」の札として排他ロックする
stream.Lock(0, 1);
try
{
// ここで本体の読み書きをする
}
finally
{
// 閉じる前に必ず解除する
stream.Unlock(0, 1);
}
この形は、同じ約束で動くアプリ同士なら有効です。ただし上のとおり、相手がメモリマップ経由なら効きませんし、そもそも他システムがこの札を見てくれる保証もありません。だから 4 章の受け渡しプロトコル側が本体になります。
4. ベストプラクティス
先に、3 章のアンチパターンとの対応を並べておきます。どれか 1 つを踏んでいる自覚があるなら、対応する節から読んでも通ります。
| アンチパターン | 何が起きるか | 対応する対策 |
|---|---|---|
3.1. Exists -> Create の二段階チェック |
確認と確保の隙間に割り込まれ、2 つのプロセスが同時に進む | 4.3 claim を原子的に取る(rename か FileMode.CreateNew) |
| 3.2. 最終ファイル名へ直接書く | 書き込み途中のファイルを受信側が読む | 4.1 temp -> close -> rename / replace で公開する |
| 3.3. ファイルサイズが止まったら完了扱い | コピーの一時停止を完了と誤判定する | 4.2 done / manifest で完了を明示する |
| 3.4. 共有ファイルをみんなで更新する | 後から書いた側で上書きされ、更新が消える | 4.3 で書き手を 1 つに絞り、4.5 で二重処理を吸収する。それでも足りないなら 6 章の撤退判断 |
| 3.5. ロックAPIを万能と思う | 約束を守らない相手やメモリマップ経由で破られる | 4.4 lease としての lock file と、4.5 idempotency で受け止める |
4.1. temp -> close -> rename / replace で公開する
王道です。 生成中のファイルは temp 名に閉じ込め、close したあとで final 名に切り替えます。 受信側は final 名だけを見るようにします。
flowchart LR
A[一意な temp 名を作る] --> B[temp に全内容を書き込む]
B --> C[flush / close する]
C --> D[同一ディレクトリで final 名へ rename / replace]
D --> E[受信側は final 名だけを監視]
ポイント:
- temp と final は 同じディレクトリ、少なくとも同じボリューム / ファイルシステム に置く
- Windows / .NET なら
File.Replace系を検討できる - final 名が見えた時点で、内容は完成済み という約束にする
temp を別ドライブに置くと、rename が単なるコピー相当になったり、Replace が失敗したりします。
この前提は地味ですが、とても大事です。
共有フォルダ(SMB)越しでは、さらに次の 4 点が揺れます。この記事の主戦場はまさにそこなので、分けて書いておきます。
- 同じ共有の同じディレクトリ内での rename は、サーバー側で実行されます。 なので「途中の名前が見えない」という性質そのものは保たれます。逆に
\\server\shareAから\\server\shareBのように共有をまたぐと別ボリューム扱いになり、Windows のMoveFileExはMOVEFILE_COPY_ALLOWEDを指定した場合、コピーと削除で移動を代替します。つまり原子的ではなくなり、途中状態が見えます。temp と final、incomingとprocessingを同じ共有の中に置く前提は、ローカルのとき以上に重要です - rename は「誰かが開いているだけ」で失敗します。 共有フォルダには、ウイルス対策、検索インデクサー、他拠点のクライアントなど、こちらが把握していない相手がアクセスしてきます。publish と claim の rename は、失敗を異常ではなく通常の分岐として扱い、短い待機を挟んでリトライする形にしておくのが現実的です
- タイムスタンプは判断材料になりません。 Microsoft Learn の File Times には、ファイル時刻について保証されるのは「変更を行ったハンドルを閉じた時点で正しく反映されること」だけだ、と書かれています。書き込み中の最終更新時刻は、書き込み用のハンドルがすべて閉じるまで完全には更新されません。粒度もファイルシステム次第で、FAT の最終更新時刻は 2 秒単位、NTFS の最終アクセス時刻は最大 1 時間遅れて更新されます。さらに SMB 越しではサーバー側の時計で時刻が付くため、クライアントとサーバーの時計がずれていれば「更新から N 分経ったら処理する」という判定もそのままずれます。完了判定を時刻やサイズでやらず、4.2 の
done/ manifest でやる理由がここにあります - 変更通知も取りこぼします。 共有フォルダの監視をイベント通知だけに頼らず、定期的なディレクトリ列挙と併用しておくと安定します。この話は FileSystemWatcher実務ガイド - 取りこぼしと重複対策 にまとめてあります
4.2. done / manifest で完全性を明示する
データ本体だけでなく、「何が完成したか」を別ファイルで明示すると、受信側が安定します。 特に異種システム連携では有効です。
flowchart TD
A[data.tmp を生成] --> B[data.csv に公開]
B --> C[data.done / manifest.json を作成]
C --> D[受信側が done / manifest を検知]
D --> E[ファイル名・サイズ・ハッシュを検証]
manifest に入れておきたいのは、このあたりの項目です。
- 対象ファイル名
- サイズ
- ハッシュ
- レコード数
- 連携ID / idempotency key
- 生成時刻
順序も大事です。
本体の公開より先に done を置くと、それは完了通知ではなく 事故予告 になります。
4.3. 受信側は claim を原子的に取る
複数ワーカーが同じ incoming を見るなら、「読む前に自分のものへ移す」のが分かりやすいです。
incoming から processing/<worker>/ への rename が成功したワーカーだけが処理します。
sequenceDiagram
participant W1 as ワーカー1
participant W2 as ワーカー2
participant IN as incoming
participant PR as processing
W1->>IN: a.csv を見つける
W2->>IN: a.csv を見つける
W1->>PR: a.csv を rename
W2->>PR: a.csv を rename
Note over W1,W2: 先に成功した方だけが所有権を取る
運用上は、ディレクトリも分けておくと追跡しやすいです。
flowchart LR
T[temp] -->|publish| I[incoming]
I -->|claim| P[processing]
P -->|成功| A[archive]
P -->|失敗| E[error]
claim 用の rename も、同じファイルシステム上で行うのが前提です。
4.4. lock file に頼るなら lease にする
lock file を使うなら、単なる空ファイルではなく 有効期限付きの所有情報 にします。 誰が取ったのか分からない lock は、後で必ず揉めます。
flowchart TD
L[lock.json] --> A[ownerId]
L --> B[host]
L --> C[pid]
L --> D[acquiredAt]
L --> E[expiresAt]
L --> F[heartbeatAt]
ポイント:
- 作成は原子的に行う
- 更新停止を stale 判定の材料にする
- 削除は 原則として作成者だけ が行う
- 解除漏れを前提に、回復手順を決めておく
lock file はあくまで 協調のための札 です。 これ1枚で完全な整合性まで保証しようとすると、だいたい厳しくなります。
4.5. idempotency を前提にする
排他制御は大事ですが、実運用では「たまに二重で来る」「途中で再実行する」をゼロにはできません。 最後は、同じ入力をもう一度食べても壊れない 設計が効きます。
flowchart LR
A[入力 + idempotency key] --> B{既処理か}
B -- はい --> C[二重実行せず成功扱い]
B -- いいえ --> D[処理を実行]
D --> E[処理済み台帳に記録]
たとえば、受信ファイルごとに連携IDを持たせ、処理済み台帳に記録します。 排他が一度破れても、結果が二重計上されない形にしておくと運用がかなり楽です。
5. 擬似コード(抜粋)
ここから出てくる MakeTempPathSameDirectory や TryClaimBundleByRename は、順序を見せるために置いた 架空の関数名 です。実際に動く実装は、冒頭で紹介したサンプル一式にあります。
この擬似コードの実装(ライブラリ、2 ワーカーの claim 競合デモ、ユニットテスト) - komurasoft-blog-samples (GitHub)
5.1. 典型的な失敗パターン
var lockPath = finalPath + ".lock";
if (!File.Exists(lockPath))
{
File.WriteAllText(lockPath, "");
using var writer = OpenForWrite(finalPath); // 最終名に直接書く
WritePayload(writer);
File.Delete(lockPath);
}
問題点は3つあります。
ExistsとWriteAllTextが別操作finalPathが書き込み途中から見えてしまう- 異常終了時に
lockが残る
5.2. 正しい方向の例(雑に書くとこう)
var tempPath = MakeTempPathSameDirectory(finalPath);
WritePayload(tempPath);
FlushAndClose(tempPath);
PublishByRenameOrReplace(tempPath, finalPath); // 同一FS / 同一volume 前提
PublishDoneFile(finalPath + ".done", new
{
FileName = Path.GetFileName(finalPath),
Size = GetFileSize(finalPath),
Hash = ComputeHash(finalPath),
IdempotencyKey = integrationId
});
if (!TryClaimBundleByRename(baseName, incomingDir, processingDir))
{
return; // 他ワーカーが先に取得
}
var manifest = ReadDoneFile(Path.Combine(processingDir, baseName + ".done"));
VerifyPayload(Path.Combine(processingDir, baseName), manifest);
if (AlreadyProcessed(manifest.IdempotencyKey))
{
MoveBundle(processingDir, archiveDir, baseName);
return;
}
Process(Path.Combine(processingDir, baseName));
RecordProcessed(manifest.IdempotencyKey);
MoveBundle(processingDir, archiveDir, baseName);
このあたりは 実装の細部より順序 が大事です。 「書く」「公開する」「所有権を取る」「処理済みを記録する」を混ぜない方が壊れにくくなります。
6. ざっくり使い分け
- 単一 writer / 単一 reader / 同一ホストなら、まずは
temp -> renameだけでもかなり安定する - 複数 consumer がいるなら、
incoming -> processingの claim rename を入れる - 異種システム連携、NAS、共有フォルダなら、manifest / done と idempotency まで入れた方が安全
- 複数 writer が同じ論理状態を更新したいなら、ファイル連携で頑張りすぎず DB やキューも検討する
- OS ロックは、同一アプリ群・同一前提の中では有効だが、受け渡しプロトコルの代わりにはならない
最後の1項目は撤退判断でもあります。 ファイルでやるとつらい問題は、本当にあります。
7. まとめ
ファイル連携の排他制御は、ロック関数を呼ぶことではなく状態遷移を決めること。これがこの記事の骨子です。生成中 / 公開済み / 処理中 / 処理済み を名前やディレクトリで表現し、Exists -> Create の二段階チェックや最終ファイル名への直接書き込み、サイズ安定待ち、共有ファイルの相互更新、ロックAPIへの過信を避ける。そのうえで temp -> close -> rename / replace、done / manifest、claim rename、lease と idempotency を組み合わせれば、共有フォルダ連携の事故はかなり防げます。
ファイル連携では「読めること」と「読んでよいこと」を同じにしないのがコツです。ここを分けるだけで、夜中にだけ出るタイプの事故がぐっと減ります。
8. 参考資料
- この記事のサンプルコード一式(ライブラリ、デモ、ユニットテスト) - komurasoft-blog-samples (GitHub)
- LockFileEx function (Win32)
- Locking and Unlocking Byte Ranges in Files (Win32)
- Moving and Replacing Files (Win32)
- MoveFileEx function (Win32)
- File Times (Win32)
- FileStream.Lock Method (.NET)
- File.Replace Method (.NET)
- rename — POSIX
- open — POSIX (
O_CREAT | O_EXCL) - flock(2) — Linux manual page
- open(2) — Linux manual page
関連する記事
同じタグを共有する最新の記事です。さらに近い話題で知識を深められます。
FileSystemWatcher実務ガイド - 取りこぼしと重複対策
FileSystemWatcher の使い方と注意点を、取りこぼし、重複通知、完了判定の落とし穴、再スキャン、原子的 claim、idempotency の観点から整理します。
業務システムのコード設計 ── 商品コード・顧客コードの決め方とチェックディジット
商品コード・顧客コードなど業務システムのコード体系を決める実践ガイド。有意コードと無意味連番の判断表、JAN・Luhn等のチェックディジット算式とC#実装、Excelの0落ち対策、桁あふれと移行まで整理します。
ADR(Architecture Decision Record)入門 ── 小規模開発で「なぜこの設計にしたか」を残す最小の方法
コードは「なぜそうしたか」を語りません。ADR(Architecture Decision Record)で設計判断の理由を1決定=1ファイルのMarkdownとして残す方法を、テンプレート、書く/書かないの判断表、実例つきで解説します。
WindowsアプリのWeb化、しない方がいいケース ── 判断表と「分割」という現実解
装置連携・ローカルファイル処理・オフライン運用を抱えたWindowsアプリでは、Web化がコスト増と機能劣化を招くことがあります。Web化に向く・向かないの判断表と、一部だけWebに出す分割構成という現実解を整理します。
CSVは「ただのテキスト」ではない ── C#業務アプリのCSV実務(文字コード・Excel互換・インジェクション対策)
業務アプリのCSV入出力の典型事故──Split(',')自作パース、BOMなしUTF-8の文字化け、先頭ゼロ落ち──を整理し、RFC 4180のルール、Shift_JISの扱い、TextFieldParserによる安全な読み込みまで解説します。
関連トピック
このテーマと近いトピックページです。記事を起点に、関連するサービスや他の記事へ進めます。
Windows技術トピック
Windows 開発、不具合調査、既存資産活用の技術トピックをまとめた入口です。
このテーマがつながるサービス
この記事は次のサービスページにつながります。近い入口からご覧ください。
Windowsアプリ開発
共有フォルダ連携や夜間バッチを含む Windows アプリ開発では、排他制御の設計がそのまま実装品質に効きます。
技術相談・設計レビュー
ロック、原子的 claim、idempotency の責務分割を先に整理したい場合は、技術相談・設計レビューとして扱えます。
よくある質問
この記事のテーマについて、相談時によくある質問をまとめています。
- ファイル連携の排他制御はロックAPIだけで十分ですか?
- 不十分なことが多いです。Linuxのflockはadvisory lockなので約束を無視する相手は普通に書けますし、Windowsのbyte-range lockはメモリマップファイルでは無視されます。OSロックは同一アプリ群・同一前提の中では有効ですが補助として使い、temp -> rename、done/manifest、原子的claim、idempotencyといった受け渡しプロトコルの設計を本体にするのが基本です。
- 書き込み途中のファイルを読まれないようにするには?
- temp -> close -> rename/replace で公開するのが王道です。生成中のファイルはtemp名に閉じ込め、closeしたあとで同一ディレクトリ上でfinal名に切り替え、受信側はfinal名だけを見るようにします。tempとfinalは同じディレクトリ、少なくとも同じボリューム/ファイルシステムに置くことが前提で、final名が見えた時点で内容は完成済みという約束にします。
- 複数のワーカーが同じファイルを同時に処理するのを防ぐには?
- 読む前にclaimを原子的に取ります。具体的にはincomingからprocessing/<worker>/へのrenameが成功したワーカーだけが処理する形にします。Exists -> Create の二段階チェックは「確認」と「確保」が別操作になっていて間に他プロセスが割り込めるため、排他になりません。原子的作成が必要なら.NETのFileMode.CreateNew系やPOSIXのO_CREAT | O_EXCLを使います。
- lock fileを使うときの注意点はありますか?
- 単なる空ファイルではなく、ownerId、host、pid、acquiredAt、expiresAt、heartbeatAtを持つ有効期限付きのlease(所有情報)にします。作成は原子的に行い、更新停止をstale判定の材料にし、削除は原則として作成者だけが行い、解除漏れを前提に回復手順を決めておきます。lock file 1枚で完全な整合性を保証しようとせず、最後はidempotencyで受け止める設計が実務では強いです。