ホーム第4章: オペレーティングシステム
第4章 3節

並行処理と排他制御

スレッドを使えば、同じメモリを共有しながら高速に並行処理を行えます。しかし、この「メモリの共有」は、極めて深刻で追跡困難なバグの温床でもあります。複数のスレッドが同じデータに殺到する「競合状態」の恐ろしさと、それを正しく統制する「排他制御(ロック)」、そしてロックが引き起こす新たな罠「デッドロック」を学びます。

1. 共有データの破壊:競合状態(Race Condition)

複数のスレッドが、同じメモリ上のデータに対して同時に読み込みや書き込み(更新)を行おうとするとき、実行のタイミング(スレッドの切り替え順序)によって結果がめちゃくちゃになってしまう現象を競合状態(Race Condition:レースコンディション)と呼びます。

具体的な例として、「銀行口座の残高(初期値 1,000円)から、同時に1,000円を引き出す」という処理を考えてみましょう。

時間 銀行口座残高 1,000 円 A: 残高1,000円を読取 A: 引出可能と判定 A: 残高を 0円に更新 B: 残高1,000円を読取 B: 引出可能と判定 B: 残高を 0円に更新 結果: 2,000円引き出せてしまった!
図 4-3:排他制御がない場合の競合状態(ダブル引き出し問題)の推移

スレッドAとスレッドBがほぼ同時に処理を開始した場合、スレッドAが残高を読み取って「引出可能(1,000円 ≧ 1,000円)」と判定した直後、まだAが書き込みを行う前にスレッドBが割り込んできます。スレッドBもまた古い残高「1,000円」を読み取るため、「引出可能」と誤って判定してしまいます。

結果として、AもBも1,000円ずつの引き出しに成功し、銀行口座からは合計2,000円が引き出されたのにもかかわらず、最終残高は「0円」と記録されます。銀行にとっては大赤字の恐ろしいバグです。

2. 解決策:排他制御(Mutual Exclusion)とロック

この問題を避けるためには、「あるスレッドがデータを操作している間は、他のスレッドは手出しできないようにする」というルールが必要です。この仕組みを排他制御(Mutual Exclusion)と呼びます。

排他制御を行うため、同時に1つのスレッドしか実行してはいけないコードの領域(クリティカルセクション)に対して、「ロック(Lock:またはミューテックス/Mutex)」と呼ばれる電子的な「鍵」をかけます。

スレッド A (鍵を所有・実行中) 口座データ (ロック中) Locked スレッド B (鍵が空くまで待機)
図 4-4:ロック(排他制御)によって順番待ちを強制されるスレッドの挙動
  1. スレッドAが口座データの操作を開始する前に、その口座の「鍵」を獲得します(ロック状態にする)。
  2. スレッドBが割り込んできて口座を操作しようとしますが、すでにロックされている(鍵がない)ため、Bは処理を一時停止し、Aが鍵を返すのを待ちます(ブロック状態)。
  3. Aが「読み取り ➔ 判定 ➔ 書き込み」の処理をすべて終えたら、鍵を返却します(アンロック)。
  4. 待機していたBがようやく鍵を獲得し、安全に更新された新しい残高「0円」に対して処理を開始します。このとき、Bは「引出不可(0円 < 1,000円)」と正しく判定でき、2重引き出しは防がれます。

3. 排他制御の副作用:デッドロック(Deadlock)

ロックを使えば安全になりますが、ロックの掛け方を間違えると、プログラムが完全にフリーズして動かなくなるバグを引き起こします。その代表例がデッドロック(Deadlock)です。

デッドロックは、2つのスレッドが互いに相手が持っている鍵を永久に待ち続けることで発生します。

デッドロックの発生シナリオ
  • スレッド 1: 鍵A を獲得 ➔ 次に 鍵B を獲得しようとして、スレッド2が解放するのを待つ。
  • スレッド 2: 鍵B を獲得 ➔ 次に 鍵A を獲得しようとして、スレッド1が解放するのを待つ。

スレッド1はスレッド2が鍵Bを返すのを待ち、スレッド2はスレッド1が鍵Aを返すのを待っています。お互いに相手の解放を待ち続けるため、どちらの処理も1ミリも前に進まなくなります。これがデッドロックです。

デッドロックを防ぐためには、「常に決められた順序でしか鍵を獲得しない(例:必ずAを獲ってからBを獲る)」といった厳格な設計ルールが必要になります。

次のセクションでは、OSのもう1つの重要な機能である、データを消えない形でディスク上に格納し管理する「ファイルシステム」の仕組みについて学びます。

現実世界と繋ぐ:チケット購入サイトの「売り切れ」を防ぐ仕組みとデッドロック

人気のライブチケットの販売開始直後、数万人が同時に購入ボタンを押します。このとき、サーバーの裏側では凄まじい並行処理と排他制御が行われています。

  • 二重販売(オーバーセリング)の防止: 「残り1枚」のチケットに対して同時に2つの購入リクエストが来た際、データベースに排他ロックをかけることで、先に到達した処理を優先し、もう一方を安全に「売り切れ」エラーにします。もしロックがなければ、1つの座席が2人に販売されてしまいます。
  • 銀行送金のデッドロック障害: 過去に実際にあった銀行のシステム障害では、「AさんからBさんへの送金(AをロックしてBをロック)」と「BさんからAさんへの送金(BをロックしてAをロック)」がミリ秒単位で完全に同時に発生し、デッドロックを起こしてシステム全体がフリーズした事例があります。

私たちがスムーズにオンライン決済や予約ができるのは、OSやデータベースの排他制御が、ミリ秒以下の世界でバグ(競合状態やデッドロック)を必死に回避してくれているおかげなのです。