同時更新で値が消えるのはなぜ?|排他・デッドロック・キュー・優先度逆転
二つの処理が一回ずつ値を増やしたのに、結果が一回分しか増えないのはなぜでしょうか。一つの命令のように見えても、読出し・計算・書戻しが別に行われることがあります。まず同時実行の順序を並べます。
事例:受付件数を二つのスレッドで増やす
共有カウンタは10です。AとBはどちらも「値を読んで1を足し、書き戻す」処理を一回実行します。ここでは各スレッドが読んだ値を手元に保持し、書戻しまで他の処理が割り込めるものとします。
Aが10を読み、Bも10を読み、Aが11を書き、Bも11を書けば最終値は11です。二回の受付があったのに、一回分の更新が失われました。期待する12にはなりません。
共有値の読出しから書戻しまでが一体でないと、両者が同じ古い値を使います。
並行と並列、原子的な更新を分ける
- 並行
複数の仕事が進行中で、処理が交互に実行される場合も含みます。
- 並列
複数の処理が同じ時刻に実行されることです。
- 原子性
対象とする操作を、他の処理から途中の状態が見えるように分割しない性質です。
単一CPUでも、読出しと書戻しの間で切り替われば競合します。複数コアだけの問題ではありません。また、複数のCPUを使えることと、共有値の更新が安全なことは別です。
変数の読み書きが一回でできても、読んで増やして戻す一連の処理まで原子的とは限りません。使用言語・CPU・同期APIの保証を確認し、見た目の短さで判断しません。
ロックは、どこからどこまで必要?
この事例では読出し・加算・書戻しの全体を同じロックで守ります。Aが終わるまでBはその範囲へ入れず、Bは11を読んで12を書きます。共有値へアクセスするすべての更新者が同じ規則を守る必要があります。
ロックを取得
共有値を読む
1を加える
共有値へ戻す
ロックを解放書戻しだけをロックしても、同じ古い値を読んだ問題は解消しません。原子的な加算APIを使う方法もありますが、複数の値をまとめて整合させるなら、その範囲に対応した同期が必要です。
例外や途中のreturnでも解放する仕組みを使います。ロック中に長い通信や通知先の処理を呼ぶと、待ち時間や循環した待ちを増やす場合があります。保護すべき状態と長い作業を分けて設計します。
排他と、処理順の通知は同じ?
ミューテックスは共有資源を一度に一つの実行主体だけが使うための仕組みです。セマフォは利用可能な個数や通知を管理でき、ミューテックスと所有権や優先度継承の扱いが異なる場合があります。
条件変数等で待つ場合は、待機の前後に同じ同期規則で条件を確認します。通知を受けても条件が成立しているとは限らないため、通常はループで再確認します。単なるsleepは他の処理の完了を保証しません。
可視性の規則も必要です。一方が書いた値を他方がいつ正しく読めるかは、言語や同期機構に依存します。変数に特殊な修飾を付けただけで、複数段階の更新をすべて排他できるとは限りません。
二つのロックで、互いに待ち続ける
AはR1を持ってR2を待ち、BはR2を持ってR1を待ちます。互いが解放しなければ進めない循環ができ、通常の待機から戻れないデッドロックになります。
処理 | 保持中 | 取得待ち | 誰の完了が必要か |
|---|---|---|---|
A | R1 | R2 | R2を持つB |
B | R2 | R1 | R1を持つA |
全処理でR1→R2のように取得順を統一すれば、この逆順取得による循環を防げます。ただし、通知や他の資源を含めて別の循環を作っていないかも確認します。
タイムアウトは待ちを検出して回復するきっかけになりますが、戻った後に保持資源を解放し、部分処理をどう扱うかを決めなければ十分ではありません。一定時間待つだけで正しい処理が完了するわけではありません。
キューに分ければ、共有値を減らせる?
受付側がイベントをキューへ送り、集計側だけがカウンタを更新する設計にできます。更新担当を一つにすることで、カウンタを複数の処理が直接変更する競合を減らせます。
- 1. イベント投入
- 2. イベント投入
- 3. 順に取り出す
キューは処理を分離しますが、容量と到着順、欠落時の扱いを決める必要があります。
キュー自体は並行アクセスに対応した機構を使います。投入できたこと、取り出されたこと、集計が完了したことは別です。途中の停止でも処理する必要があるなら、保存や再試行、重複の識別も設計します。
バッファを増やせば、欠落はなくなる?
空のバッファへ平均20件/秒が入り、平均12件/秒を処理するモデルなら、毎秒8件分の未処理が増えます。32件の容量を平均差だけで見積もると、32/8=4秒でいっぱいになります。
これは滑らかな平均流量を使った概算です。突発的な入力や処理停止があれば、より早く満杯になることがあります。継続的に入力が処理能力を超える場合、有限の容量を増やしても時間稼ぎにしかなりません。
満杯なら待たせる、拒否する、古いデータを置換する、永続領域へ退避する等から要件に合う方針を決めます。欠落を許さない受付件数と、最新値だけ必要な表示では、適切な方針が異なります。
高優先度の処理が、中優先度に負ける理由
低優先度Lがロックを持ち、高優先度Hがそのロックを待っています。中優先度MがLを中断すると、HはMの処理が終わるまで間接的に待つ場合があります。これが優先度逆転を長引かせる経路です。
優先度継承では、Hが待っている間、保持者Lの優先度を引き上げ、Mに邪魔されず解放へ進みやすくします。すべてのミューテックスが同じ機能を持つわけではなく、実装の条件を確認します。
優先度継承は、逆順のロック取得によるデッドロックを自動で除く仕組みではありません。保持時間を短くする、取得順を統一する、最悪の待ち時間を見積もる等を併せて行います。
演習1:更新の消失
条件:AとBがどちらも10を読み、各自で11を作って書き戻します。
問い:最終値と、期待値を答えてください。
解答例:最終値は11、二回の加算を反映した期待値は12です。
根拠:後の書戻しも11なので、先の一回分が結果に残りません。
誤答の理由:二回処理したから必ず12という判断は、実行順と読んだ値を追えていません。
演習2:保護する範囲
条件:読出しはロック外で、書戻しだけをロックします。
問い:問題は解決しますか。
解答例:解決しません。読出しから加算・書戻しまでを同じロックで保護します。
根拠:二つの処理がロック前に同じ値を読む可能性が残ります。
誤答の理由:同時の書込みさえ禁止すればよいという説明では、古い値を元にした更新を防げません。
演習3:待ちの循環
条件:AはR1を保持しR2待ち、BはR2を保持しR1待ちです。
問い:原因と予防策を示してください。
解答例:互いの保持資源を待つ循環が原因です。全処理の取得順をR1→R2へ統一します。
根拠:逆順に一つずつ保持する状況を防ぐと、この循環を作れません。
誤答の理由:優先度を上げるだけでは、相手が解放するまで必要な資源を取得できません。
演習4:キュー容量
条件:空の32件バッファへ平均20件/秒を投入し、平均12件/秒を処理する概算モデルです。
問い:平均差から満杯までの時間を求めてください。
解答例:32/(20-12)=4秒です。
根拠:処理して減る分を引いた毎秒8件が未処理として残ります。
誤答の理由:32/20は処理による減少を無視します。この概算だけで突発入力への耐性は保証できません。
演習5:優先度逆転
条件:LがHの必要なロックを保持し、MがLを中断します。
問い:優先度継承は何を改善しますか。
解答例:Lの優先度をHに合わせて引き上げ、Mによる中断を抑えてロックの解放を進めます。
根拠:Hが待つ原因はLの未完了で、保持者を実行させる必要があります。
誤答の理由:Hだけをさらに高くしても、ロックが解放されるまで実行できない条件は変わりません。
演習6:通知を受けた後
条件:条件変数で「キューが空でない」を待つ処理が通知を受けました。
問い:そのまま取り出してよいですか。
解答例:同じ同期規則で空でないかを再確認し、必要なら再び待ちます。
根拠:別の処理が先に取り出した場合など、通知後も条件が成り立つとは限りません。
誤答の理由:通知が来たから一件は必ず自分のものと判断すると、空のキューを扱う競合を残します。
出典と仕様を確認する
FreeRTOS公式:ミューテックス・優先度逆転・デッドロック
関連テーマを続けて学ぶ
次におすすめの学習
編集・検証について
編集・検証:IT資格ラボ編集部
IPAが公開する試験要綱・シラバス・過去問題と、各技術の公式資料を優先して内容を確認しています。制度変更や誤りを確認した場合は、記事を見直して更新します。
編集方針・情報源・訂正方針を見る