TL

カーネルのロックフリー同期とCAS

ロックを使わずに並行データ構造を安全に更新する仕組みを、CASという1命令から原理で理解できます。ABA問題やウェイトフリーとの違い、楽観的同時実行の考え方まで一気につかめます。

応用ロックフリーCASアトミック操作ABA問題並行処理カーネル最終更新: 2026-06-22
TL;DR要点だけ先に
  • 1.CASは「今の値が期待どおりなら新値に書き換える、違えば失敗」を不可分に行うCPU命令。これを失敗したら読み直して再試行するループにするのがロックフリーの基本形。
  • 2.ロックフリーは「どれかのスレッドは必ず前進する」保証。ウェイトフリーは「全スレッドが有限ステップで完了する」より強い保証で、両者は別物。
  • 3.CASは値の一致しか見ないため、A→B→Aと戻ると変化を見逃すABA問題が起きる。世代カウンタやハザードポインタ、RCUで対策する。

なぜロックフリーなのか──ロックの構造的な弱点

ロック(ミューテックス)は正しく安全ですが、構造的な弱点を抱えています。鍵を握ったスレッドが、解放する前に スケジューラに横取り(プリエンプト) されると、その間ほかの全スレッドは何もできずに待たされます。優先度の高いスレッドが、鍵を持つ低優先度スレッドの再実行を待つ 優先度逆転 も起きえます。最悪、鍵を持つスレッドが落ちれば、その鍵は永久に返ってきません。

ロックフリー同期は、この「1スレッドの停止が全体を巻き込む」性質を断ち切る設計です。鍵という共有状態を持たず、CPUのアトミック命令だけで 共有データを更新します。土台となる排他制御の全体像は 排他制御とデッドロック を、横取りが起きる仕組みは コンテキストスイッチ を参照してください。

CAS:ロックフリーの土台となる1命令

ロックフリーの心臓部が CAS(Compare-And-Swap、比較交換) です。「指定アドレスの値が期待値と一致するなら新値に書き換え、一致しなければ何もしない」を、途中で割り込まれない1つの不可分な操作として実行します。

CAS(addr, expected, new) -> bool:
  アトミックに {
    if *addr == expected:
        *addr = new
        return true        # 成功
    else:
        return false       # 失敗(誰かが先に書き換えた)
  }

x86 では LOCK CMPXCHG、ARM では LDXR/STXR(LL/SC)として実装されます。重要なのは、CAS が 失敗を返せる こと。失敗は「自分が値を読んでから書こうとするまでの間に、別スレッドが割り込んで更新した」という事実を意味します。この失敗を起点に 読み直して再試行 するのがロックフリーの基本パターンです。

do {
    old = atomic_load(&counter)      # 現在値を読む
    new = old + 1                    # 望む新値を計算
} while (!CAS(&counter, old, new))   # old のままなら成功、変わっていたらやり直し
CASは「楽観的」──まず競合しない前提で進む

ロックは「衝突するかもしれないから先に守る」悲観的 な戦略。CASは「たぶん衝突しないから、まず計算して最後に確かめる」楽観的 な戦略です。競合が稀ならロック取得・解放のコストがまるごと消え、競合が激しいと再試行が増える。この性質はDBの楽観的ロック(バージョン番号で衝突検知)と同じ発想で、根は1つです。

楽観的同時実行の原理:read-copy-update のループ

CASループの本質は 「読む→新しい状態を作る→一致を確認して差し替える」 の繰り返しです。共有データに直接手を入れず、自分のローカルで望む結果を組み立て、最後の一瞬だけCASで原子的に公開します。

連結リストの先頭に要素を挿入する例で見ます。

node->next = head             # ① 今の先頭を新ノードの次に繋ぐ(ローカル操作)
while (!CAS(&head, node->next, node)) {
    node->next = head         # ② head が変わっていたら読み直してやり直す
}

ポイントは、CASが守る不変条件です。「自分が node->next に記録した先頭と、いま実際の head が同一なら、その間に誰も head を変えていない」とみなせる。だから安全に差し替えられます。誰かが先に挿入していれば head が変わってCASが失敗し、新しい先頭で組み直す。ロックを取らずに、衝突したスレッドだけがやり直す わけです。

単一CASで守れるのは「1ワード」だけ

CASが原子的に書き換えられるのは基本的に ポインタ1個ぶん(1ワード) です。だからロックフリー構造は「最後の公開を1個のポインタ差し替えに集約する」よう設計します。複数ワードを一括更新したい場合は、隣接2ワードを扱う DWCAS(double-width CAS、x86の CMPXCHG16B や、後述の世代カウンタの埋め込み、あるいはトランザクショナルメモリ(HTM)に頼ることになります(離れた2アドレスを同時に扱うDCASはハードウェアでは一般に提供されません)。

ロックフリーとウェイトフリーは別物

「ロックを使わない=ロックフリー」ではありません。ノンブロッキングな進行保証は 段階 で定義され、混同は禁物です。

進行保証保証の内容代償・特徴
ブロッキング(ロック)鍵を持つスレッドが止まると、待つ側は前進できない実装は素直だが、横取り・優先度逆転に弱い
オブストラクションフリー他スレッドが止まれば、単独実行なら有限ステップで完了最弱。競合下ではライブロックしうる
ロックフリーシステム全体として、常にどれか1スレッドは前進する個々のスレッドは飢える可能性がある(再試行し続ける)
ウェイトフリー全スレッドが他に関係なく有限ステップで完了する最強。飢餓なし。実装は格段に難しく重いことが多い

差は 「誰の前進を保証するか」 にあります。ロックフリーが保証するのは「システム全体の前進」だけ。CASループでは、ある不運なスレッドが毎回CASに失敗し続けて 永久に再試行する(飢餓に陥る) ことが原理上ありえます。それでも「誰か」は必ず成功して進むため、全体は止まりません。

ウェイトフリーはさらに踏み込み、「各スレッドが、他の挙動に関わらず決まった上限ステップで必ず完了する」を保証します。飢餓が起きません。実現には、止まりかけた他スレッドの作業を 手伝って肩代わりする(helping) などの工夫が要り、コストも複雑さも跳ね上がります。リアルタイム系で最悪実行時間の上限が要る場面以外では、実務上ロックフリーで十分なことが多いです。

「ロックフリー」を「速い」と同義に思わない

ロックフリーは 進行保証 の概念であって、性能の保証ではありません。競合が激しいとCASの再試行が増え、キャッシュライン上の同じ行を全コアで奪い合う キャッシュコヒーレンスのトラフィック が膨らみ、かえってロックより遅くなることもあります。「正しさと進行性のための設計」であり、速度は計測して初めて言えるものです。

ABA問題:CASが見ているのは「値」だけ

ロックフリー実装で最も悪名高い罠が ABA問題 です。原因は単純で、CASは「値が一致するか」しか見ない こと。値がいったん別物に変わってから元に戻ると、CASは「変化していない」と誤認します。

ロックフリースタックの pop で起きる典型を追います。

スレッド1: top を読む → A(次は B)。CAS(&top, A, B) を出そうとした直前で横取りされる
スレッド2: A を pop(top は B に)→ B も pop(top は C に)→ A を再利用して再び push(top は A に、ただし A->next は今や C)
スレッド1: 再開。CAS(&top, A, B) は「top はまだ A だ」と見て成功してしまう
        → top を B に書き換える。しかし B はすでに解放済み。リストが壊れる

top の値はA→B→…→Aと 一周して戻った ため、CASは中身が入れ替わった事実に気づけません。A->next がもう B を指していないのに、古い前提のまま top=B を書き込み、解放済みメモリへの参照(use-after-free)を生みます。

ABAは「再現しにくい本番限定バグ」になりやすい

ABAが成立するには「読んでからCASまでの間に、値がA→…→Aと戻る」絶妙なタイミングが要ります。低負荷のテストではまず踏まず、本番の高並行・特定スケジューリングでだけ稀に壊れる——並行バグの中でも追跡が極めて難しい部類です。さらにメモリアロケータの再利用が絡むと、解放したノードのアドレスがすぐ再利用されてABAを誘発しやすくなります。

ABA問題への対策

対策はいずれも「値の一致だけでは『変化なし』と断定しない」ための仕組みです。

手法考え方向き・コスト
世代カウンタ(タグ付きポインタ)ポインタに更新回数を同梱し、ポインタ+世代をまとめてCAS。世代が違えば一周しても検知DWCAS(CMPXCHG16B)等が要る。最も直接的。世代の周回(ラップ)には注意
ハザードポインタ各スレッドが「今参照中」と公開。公開されたノードは誰も解放しない解放を遅延し安全に回収。スキャンのコストはかかる
RCU(Read-Copy-Update)読み手はロックなし。書き手はコピーを差し替え、全読み手が抜けてから旧版を解放読み多寡が激しい場面に最適。カーネルで多用される
GC任せ解放しないので再利用されず、ABA自体が起きにくい管理環境向け。手動メモリ管理には使えない

最も直接的なのが 世代カウンタ。ポインタの隣に「更新通し番号」を置き、ポインタと世代を一括してCAS します。A→…→Aと値が戻っても世代は必ず進んでいるため、CASが正しく失敗します。x86 の CMPXCHG16B で 16 バイトをまとめて比較交換するのが定石です。

カーネルで広く使われるのが RCU です。読み手はロックもCASも使わずほぼ素通りで読み、書き手は 旧データを書き換えず新しいコピーを作って、ポインタ差し替え1発で公開 します。旧バージョンは「いま読んでいる全スレッドが読み終える(猶予期間=grace period を抜ける)」のを待ってから解放するため、参照中のメモリが消えずABAやuse-after-freeを避けられます。Linux カーネルがルーティングテーブルや設定の参照で多用する、読み取り偏重に極めて強い手法です。

試験・面接で問われる要点
  • CAS は「期待値と一致すれば書き換え、違えば失敗」を アトミック に行う。失敗時に読み直して再試行するのがロックフリーの基本形。
  • ロックフリー=「全体として常にどれかが前進」(個々は飢餓しうる)。ウェイトフリー=「全スレッドが有限ステップで完了」(より強い)。
  • ABA問題=CASが値の一致しか見ないため、A→B→Aの往復を見逃す。対策は 世代カウンタ / ハザードポインタ / RCU
  • ロックフリーは 進行保証 の話であり「速い」とは限らない。競合が激しいと再試行とキャッシュ競合で遅くなりうる。

まとめ──楽観的に進み、確認の瞬間だけ原子的に

まとめ

ロックフリー同期は、鍵という共有状態を持たず CASだけ で共有データを更新する設計です。CAS は「期待値と一致すれば書き換え、違えば失敗」を不可分に行う命令で、失敗したら読み直して再試行する 楽観的同時実行 が基本形。これにより「1スレッドの停止が全体を止める」ロックの弱点を断てます。ただし保証されるのは進行性であり、ロックフリー(全体は必ず前進、個々は飢餓しうる)と ウェイトフリー(全スレッドが有限ステップで完了)は別物。そして ABA問題——CASが値しか見ないために A→B→A の往復を見逃す罠——を 世代カウンタ・ハザードポインタ・RCU で塞ぐ必要があります。なぜCASだけで順序が守れるのかは メモリオーダリングとメモリバリア が、スレッドそのものの基礎は プロセスとスレッド が土台になります。

OS Article

カーネルのロックフリー同期とCASを実務で読む

TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。

解決すること

ロックフリー

比較で見る軸

難易度: advanced / カテゴリ: OS / タグ数: 6

導入後に効く点

ロックフリーは「どれかのスレッドは必ず前進する」保証。ウェイトフリーは「全スレッドが有限ステップで完了する」より強い保証で、両者は別物。

先に潰すリスク

用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。

数字・仕様の読み方
難易度
advanced
カテゴリ
OS
タグ数
6

判断チェックリスト

  • 自社の用途が「ロックフリー / CAS」に近いか確認する。
  • 強みである「CASは「今の値が期待どおりなら新値に書き換える、違えば失敗」を不可分に行うCPU命令。これを失敗したら読み直して再試行するループにするのがロックフリーの基本形。」が本当に評価軸になるか確認する。
  • 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
  • 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
  • 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
  • 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。

次に確認する観点

ロックフリーCASアトミック操作ABA問題並行処理ロックフリーCASアトミック操作
参考: 公式情報