📄
概念 📚 software-design-concepts

並行処理・マルチスレッド

スレッドセーフ・競合状態・デッドロック・非同期I/Oなど並行プログラミングの主要概念と設計上の注意点

並行処理とは、複数の処理を同時または交互に実行する手法であり、CPUの利用効率を高めてスループットを向上させることを目的とする。マルチスレッドやマルチプロセス、イベントループベースの非同期I/Oなど、実行モデルは言語や実行環境によって異なる。コードレビューでは、どの実行モデルを使うかだけでなく、共有リソースへのアクセス制御が正しく設計されているかを確認することが重要になる。

競合状態(Race Condition)は、複数のスレッドが同一のメモリ領域に保護なしでアクセスする場合に生じ、実行順序によって結果が変わる非決定的なバグを引き起こす。デッドロック(Deadlock)は、複数のスレッドが互いに相手の保持するロックを待ち続けて永久に進まない状態であり、ロックの取得順序を統一することで回避できる。これらのバグは再現が難しいため、設計段階での予防が特に重要となる。

非同期I/O(async/await)はシングルスレッドのイベントループ上でI/O待機を効率化するモデルであり、JavaScript/TypeScriptでは標準的なパターンとなっている。ただしCPUバウンドな処理をイベントループ上で実行するとブロッキングが発生し、スループットが低下する。アクターモデル(Erlang・Akkaで採用)は、共有メモリを排除してメッセージパッシングで状態を管理し、並行処理の複雑さを大幅に軽減する。

コードレビューで着目するポイント

  • 共有変数へのアクセスにミューテックスやatomicな操作が適切に使われているか
  • ロックの取得順序が全スレッドで統一されており、デッドロックの可能性がないか
  • スレッドセーフでないコレクション(例: ArrayList)がマルチスレッド環境で使われていないか
  • async/await を使う関数で、CPUバウンドな処理がワーカースレッドに適切にオフロードされているか
  • Promise / Future のエラーハンドリングが抜け漏れなく実装されているか
  • グローバル変数やシングルトンへのスレッドからのアクセスが安全に設計されているか
  • ロックの保持範囲が最小限に絞られており、不必要にクリティカルセクションが広くなっていないか
  • setTimeout / setInterval やイベントリスナーの登録・解除が対称に行われているか(メモリリーク防止)

典型的なアンチパターン

ロックなしの共有カウンタ更新: 複数スレッドから counter++ を同期なしで呼ぶと、読み取り・加算・書き込みの間に割り込みが入り、更新が消失する競合状態が発生する。AtomicInteger やミューテックスでの保護が必要。

デッドロックを引き起こすロック取得順序の不統一: スレッドAがロックXを取得してからYを待ち、スレッドBがYを取得してからXを待つコードは、両スレッドが永久にブロックされるデッドロックを引き起こす。ロックの取得順序をシステム全体で一方向に統一することで回避できる。

イベントループのブロッキング: Node.js などのシングルスレッドランタイムで、ファイルの同期読み込みや重いJSON.parse処理をメインスレッドで実行すると、その間に新着リクエストをすべてブロックする。重いCPU処理は worker_threads にオフロードすべき。

参考リソース

  • Java Concurrency in Practice (Brian Goetz et al.) — Javaにおける並行設計パターンとロック戦略の定番書
  • MDN Web Docs: Concurrency model and the event loop — ブラウザのイベントループモデルの公式解説
  • Node.js公式ドキュメント: Worker Threads — Node.jsにおけるCPUバウンド処理のオフロード方法
  • Akka Documentation — アクターモデルベースの並行処理フレームワークの公式ガイド
  • Go公式Tour: Concurrency — goroutine・channelによるGoのシンプルな並行モデルの入門