共有リソースへの同時アクセスを防ぐ仕組み。ロック有り/無しで結果がどう変わるかを比較します。
排他制御(mutual exclusion)とは、複数のプロセスやスレッドが共有リソースに同時にアクセスして壊さないように制御する仕組みです。
身近な例えだと、「公衆トイレの個室の鍵」のような仕組みです。トイレ(クリティカルセクション)は同時に 1 人しか使えないので、入る人は鍵を掛ける(ロック)。次に来た人は外で待つ。中の人が出てきて鍵を外す(解放)と、ようやく次の人が入れる。
上のツールで「排他制御なし」と「排他制御あり」を切替えると、ロックの有無で結果がどう変わるかを比較できます。
クリティカルセクション(critical section)とは、共有リソースにアクセスするコード区間のことです。ここで複数プロセスが同時に動くとデータが壊れるため、同時に 1 プロセスだけが入れるように排他制御をかける必要があります。
クリティカルセクションが守るべき3つの性質:
・相互排除(Mutual Exclusion):同時に 1 プロセスだけが入れる
・進行性(Progress):誰も入っていないなら、待っているプロセスは入れる
・有限待ち(Bounded Waiting):永遠に待たされない(飢餓を防ぐ)
上のツール「排他制御あり」のステップ 3 で、P1 がクリティカルセクションに入っている間、P2 が「ロック待ち」になって待たされる様子を確認できます。これが相互排除の働きです。
排他制御で最も基本的な道具がロック(mutex = mutual exclusion)です。鍵のように取得・解放を 1 対で使います。
正しい使い方:
・① lock():ロックを取得(誰かが持っていれば待機)
・② クリティカルセクションを実行:共有リソースに安全にアクセス
・③ unlock():ロックを解放(待っていた次のプロセスが目を覚ます)
注意:unlock を忘れると他のプロセスが永遠に待たされる大問題が起きます。例外発生時にも必ず unlock するよう、try-finally や RAII 等の仕組みで保証するのが定石です。
ロックなしで複数プロセスが共有変数を更新すると、更新が消失する「ロストアップデート」が発生します。これは「OSのスレッド」ページで扱ったレースコンディションの典型例です。
上のツール「排他制御なし」シナリオで、2 つのプロセスが counter を +1 しても 1 にしかならない例が確認できます。原因:
・P1 が counter(=0)を読む → レジスタに 0
・P2 も counter(まだ=0)を読む → レジスタに 0
・P2 が +1 して書き戻す → counter = 1
・P1 が古い値で +1 して書き戻す → counter = 1(P2 の更新が上書きされた)
この問題は銀行口座の残高更新などでよく例として挙げられます。同時に 1 万円ずつ振り込んだのに、結果が 1 万円増だけになると大事件です。
排他制御は身近なソフトウェアで広く使われています。
・データベースのトランザクション:レコードの同時更新を制御
・ファイル書き込み:複数プロセスが同じファイルを書く際の整合性
・プリンタ共有:複数の印刷ジョブが混ざらないよう順番に処理
・Web アプリのカウンター・在庫管理:複数リクエストの並列処理
・マルチスレッドプログラム:共有データ構造の操作(キュー、マップなど)
排他制御が必要かどうかの判断は「共有リソースに書き込みが発生するか」がポイント。読み取りだけなら不要、書き込みがあれば必要、と覚えておくと迷いません。
ロックは便利ですが、扱いを誤るとさらに厄介な問題を生みます。
・デッドロック:複数プロセスが互いのロックを待ち合い永遠に止まる(次ページで詳説)
・飢餓(starvation):特定のプロセスがいつまでもロックを取れず実行されない
・性能低下:ロックが衝突すると並列性が下がる。ロックの粒度を細かくしすぎても粗くしすぎても問題
・unlock 漏れ:例外発生で unlock しないと永久にロックされたまま
特にデッドロックは実務でも重要なので、次のページ「OSのデッドロック」で詳しく学びましょう。
なぜロックが必要なのか。それは、CPUが複数の処理を細かく交互に切り替えながら動かすため、「読む → 計算 → 書く」という一連の操作が途中で割り込まれることがあるからです。
上の図で何が起きているか見てみましょう。共有の変数(最初は 0)を、P1 と P2 がそれぞれ +1 しようとしています。
・P1 が 0 を読んでいる間に、P2 も同じ 0 を読んでしまう
・両方が「0 に 1 足して書く」ので、結果は 1 が2回書かれるだけ
・本来 2 になるべきが、1 になってしまう
この「どちらが先に動くかで結果が変わる、ギャンブルのような状態」をレースコンディション(=競合状態)と呼びます。ロックはこの競合を防ぐために、「読む〜書くまでの一連の操作を、他のプロセスに割り込まれない1かたまりにする」道具です。銀行口座に例えると、同じ残高を2人が同時に読んで別々に引き出したら、どちらかの引き出しが消えてしまうのと同じ問題です。
デッドロックとは、複数のプロセスが互いの持っているロックを待ち合って、全員が身動き取れなくなる状態のことです。ロックを正しく使わないと起きます。
上の図の例で流れを追ってみましょう。
・P1 がロック A を取得して作業中。次に ロック B も必要になった
・P2 がロック B を取得して作業中。次に ロック A も必要になった
・P1 は「B が空くのを待つ」→ しかし B は P2 が持っている
・P2 は「A が空くのを待つ」→ しかし A は P1 が持っている
・どちらも永遠に待ち続け、一切進まない
身近なたとえで言うと、狭い一本道ですれ違いができず、お互いが「相手が引いてくれるまで待つ」と主張して動けなくなった2台の車のようなものです。デッドロックを防ぐ基本的な方法は「ロックを取る順番を全プロセスで統一する」ことです。たとえば「必ず A → B の順に取る」と決めれば、図のような待ち合いは起きません。