diff --git a/CLAUDE.md b/CLAUDE.md index faf3386..3813c9d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,7 +4,7 @@ ## 開発進捗 -最新: CameraBase のリファクタリング Phase 5 完了(IScam/Lucam の重複するプレビュー・撮影処理を CameraBase にテンプレートメソッドとして集約) +最新: PLAN_02 優先度1 完了(連続撮影クラッシュの根本原因=`_shots` のスレッドセーフ化.`BlockingCollection` で生産者-消費者パイプライン化) ※ 本欄は**最新ステップ 1 行のみ上書き更新**.詳細な進捗履歴(動機・設計判断・失敗パターン)は [docs/PROGRESS.md](docs/PROGRESS.md) に追記する.運用ルールは GUIDE_05「進捗記録の運用ルール(CLAUDE.md / PROGRESS.md)」を参照. ## 必須ルール(コード実装時) diff --git a/TIASshot/Cameras/CameraBase.cs b/TIASshot/Cameras/CameraBase.cs index 10dfa26..db839c2 100644 --- a/TIASshot/Cameras/CameraBase.cs +++ b/TIASshot/Cameras/CameraBase.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.Eventing.Reader; @@ -47,7 +48,7 @@ protected bool _isPreview = false; protected List _chartMasks = new List(); protected string _saveFolder = ""; - protected List _shots = new List(); + protected BlockingCollection _shots = new BlockingCollection(); protected Dictionary _ShotInfo = new Dictionary(); // 設定値保存用 protected Rect _roi; @@ -144,11 +145,8 @@ /// protected void SaveThread(int numImages) { int saveCount = 0; - while (saveCount < numImages) { - while(_shots.Count <= saveCount) { - Thread.Sleep(1); - } - SaveImages(_shots[saveCount], saveCount); + foreach (var img in _shots.GetConsumingEnumerable()) { + SaveImages(img, saveCount); saveCount++; _form.ShowMessage($"画像{saveCount}/{numImages}枚目保存"); } @@ -189,25 +187,30 @@ /// CSV 書き込み・ループ・タイミング制御を担う. /// フレーム取得(CaptureFrame)は派生クラスに委譲する. /// BeginShot/EndShot は呼び出し元(派生クラスの Shot)で前後に実行すること. + /// ループ完了後に _shots.CompleteAdding() を呼び,SaveThread に終了を通知する. /// /// 撮影枚数 /// 撮影間隔(ms) protected void RunShotLoop(int numImages, int interval) { - var filename = Config.GetString("File/Info"); - using (var csv = new StreamWriter(Path.Combine(_saveFolder, filename))) { - WriteInfo(csv); - csv.WriteLine("Shot,Time(ms)"); + try { + var filename = Config.GetString("File/Info"); + using (var csv = new StreamWriter(Path.Combine(_saveFolder, filename))) { + WriteInfo(csv); + csv.WriteLine("Shot,Time(ms)"); - var watch = Stopwatch.StartNew(); - for (var i = 0; i < numImages; i++) { - var shotTime = watch.ElapsedMilliseconds; - csv.WriteLine($"{i + 1},{shotTime}"); - _form.ShowMessage($"撮影 {i + 1} / {numImages} 枚目"); - _shots.Add(CaptureFrame()); - while (watch.ElapsedMilliseconds < interval * (i + 1) && i < numImages - 1) { - Thread.Sleep(1); + var watch = Stopwatch.StartNew(); + for (var i = 0; i < numImages; i++) { + var shotTime = watch.ElapsedMilliseconds; + csv.WriteLine($"{i + 1},{shotTime}"); + _form.ShowMessage($"撮影 {i + 1} / {numImages} 枚目"); + _shots.Add(CaptureFrame()); + while (watch.ElapsedMilliseconds < interval * (i + 1) && i < numImages - 1) { + Thread.Sleep(1); + } } } + } finally { + _shots.CompleteAdding(); } } diff --git a/TIASshot/Cameras/IScam.cs b/TIASshot/Cameras/IScam.cs index 17ba31a..1e68838 100644 --- a/TIASshot/Cameras/IScam.cs +++ b/TIASshot/Cameras/IScam.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Text; @@ -163,7 +164,7 @@ _ic.LiveStart(); _snapSinkRef = _ic.Sink as FrameSnapSink; - _shots.Clear(); + _shots = new BlockingCollection(); _snapSinkRef.SnapSingle(TimeSpan.FromSeconds(5)); // 最初のフレームを捨てる var thread = new Thread(() => SaveThread(numImages)); diff --git a/TIASshot/Cameras/Lucam.cs b/TIASshot/Cameras/Lucam.cs index 70ef974..892364f 100644 --- a/TIASshot/Cameras/Lucam.cs +++ b/TIASshot/Cameras/Lucam.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.Linq; @@ -223,7 +224,7 @@ protected override void Shot(int numImages=1, int interval=0) { SetSnapParam(); - _shots.Clear(); + _shots = new BlockingCollection(); var thread = new Thread(() => SaveThread(numImages)); thread.Start(); diff --git "a/docs/03_PLAN/PLAN_02_\343\203\220\343\202\260\344\277\256\346\255\243\350\250\210\347\224\273.md" "b/docs/03_PLAN/PLAN_02_\343\203\220\343\202\260\344\277\256\346\255\243\350\250\210\347\224\273.md" index 1b74f0e..eb98d15 100644 --- "a/docs/03_PLAN/PLAN_02_\343\203\220\343\202\260\344\277\256\346\255\243\350\250\210\347\224\273.md" +++ "b/docs/03_PLAN/PLAN_02_\343\203\220\343\202\260\344\277\256\346\255\243\350\250\210\347\224\273.md" @@ -17,6 +17,8 @@ ### 優先度1: `_shots` へのスレッドセーフでないアクセス(根本原因) +> **状態: 完了(2026-06-07)** — `_shots` を `BlockingCollection` 化して生産者-消費者パイプラインに変更(`RunShotLoop` で `Add`/`CompleteAdding`、`SaveThread` で `GetConsumingEnumerable`).撮影ループは `try/finally` で `CompleteAdding()` を保証.詳細は `docs/PROGRESS.md` および `git log` を参照. + **原因** `_shots`(`CameraBase` の `List` フィールド)は撮影スレッドと保存スレッド(`SaveThread`)から同時にアクセスされる. diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index 21ef67b..e69cb62 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -22,3 +22,8 @@ - 方針: **挙動は一切変えない**。自動テスト不可(ハードウェア/ネイティブ/WinForms 密結合)のため、ビルド 0/0 + diff の処理順序目視一致 + 実機撮影確認で担保。 - 設計判断: `IScam` の撮影前後処理(Sink 切替・最初のフレーム捨て)は `Lucam` と実行順序が異なるため共通骨格に取り込まず派生側に残した(順序維持を優先)。 - 失敗パターン: 校正収束時のデバッグログを撮影情報記録とまとめて `CalcTcc` の後に移したことで元の「ログ→CalcTcc→記録」順がずれた → テストで検出し、`CalcTcc` 前に呼ぶ専用フックへ分離して厳密な順序一致を回復。 + +- [x] **PLAN_02 優先度1: `_shots` のスレッドセーフ化(連続撮影クラッシュ修正)** (2026-06-07): 連続撮影で4枚以上取得するとたまにクラッシュする不具合を修正。根本原因は `_shots`(`List`)を撮影スレッドと保存スレッドで無保護に共有していたこと(容量拡張時のリサイズ競合・TOCTOU)。 + - 設計判断: `ConcurrentQueue` 単体ではなく `BlockingCollection` を採用し生産者-消費者パイプライン化。`GetConsumingEnumerable`/`CompleteAdding` で `Thread.Sleep` ポーリング待機を排除。保存連番は独立カウンタで従来順序を維持。 + - 堅牢化: 撮影ループを `try/finally` で包み、例外時も `CompleteAdding()` を必ず呼んで消費者(保存スレッド)の永久ブロックを防止(例外はそのまま伝播。ユーザー向けエラー表示・撮影中断は優先度3 で別途)。 + - 回帰検証: 自動テスト不可(ハードウェア依存)のため、ビルド 0/0 + 実機(校正・連続撮影・複数回・連番一致)確認 + 設計レビューで担保。