メモリ整合性モデルとシーケンシャル一貫性
並行コードが「ハードでは動くのに理論では未定義」になる理由を、整合性モデルの階層から原理でつかめます。SC・TSO・PSO・弱順序・リリース一貫性の強弱と、言語メモリモデルがなぜ要るかを整理します。
- 1.整合性モデルは『複数コアの読み書きにどの全順序を許すか』の契約。最強のシーケンシャル一貫性(SC)はプログラム順を保ったインターリーブだけを許し、TSO→PSO→弱順序→リリース一貫性と段階的に並べ替えを解禁していく。
- 2.緩和の源はストアバッファ。TSOはStoreLoadのみ、PSOはStoreStoreも、弱順序はほぼ全方向を許す。緩いほど速いが、同期点を自分でフェンスとして指定しないと観測順序が壊れる。
- 3.ハードのモデルだけでは移植可能なコードが書けない。C++11/Javaの言語メモリモデルがacquire/releaseとデータ競合の定義を与え、SCを保証する条件(DRFなら逐次一貫)を約束するから言語レベルの抽象が要る。
整合性モデルとは「許される全順序」の契約
マルチコアでの並行プログラムを論じるとき、まず分けるべきは2つの概念です。キャッシュコヒーレンシ は「1つのアドレスに対する複数コアの書き込みが、全コアで同じ1本の順序に見える」ことを保証します(詳しくは メモリ階層とキャッシュコヒーレンシ(MESIプロトコル))。一方 メモリ整合性モデル は「複数のアドレスにまたがる 読み書きが、どんな相対順序で他コアに見えてよいか」を取り決めます。コヒーレンシは1アドレス内、整合性モデルはアドレス間の話です。
整合性モデルは、ハードウェアやプログラミング言語が並行コードに与える 形式的な契約 だと考えると正確です。契約が強い(許す並べ替えが少ない)ほど直感どおりに動きますが、最適化の余地が減って遅くなります。弱いほど速いが、プログラマが同期点を明示しないと結果が定義されません。この強弱の段階こそ、本稿で扱う モデルの階層 です。
シーケンシャル一貫性(SC)──最強で最も直感的
階層の頂点が シーケンシャル一貫性(Sequential Consistency, SC) です。Lamport の定義はこうです。
すべてのコアの操作が、ある単一の全順序に並べられ、かつ各コア内ではプログラム順が保たれる。その全順序の通りに1つの共有メモリを逐次に操作した結果と、実行結果が一致する。
要は 「各スレッドの命令列を、プログラム順を崩さずにインターリーブ(交互配置)した、何らかの1本の列」 で説明できる、ということです。並べ替えは一切起きず、ただ複数スレッドの混ざり方だけが非決定的になります。
スレッド1: x = 1; r1 = y;
スレッド2: y = 1; r2 = x;
SC で「ありえる」インターリーブの一例:
x=1 → y=1 → r1=y(=1) → r2=x(=1)
SC で「禁止される」結果:
r1==0 かつ r2==0 ← どんなインターリーブでも作れない
r1==0 かつ r2==0(両者が相手の書き込み前の値を読む)は、SC では決して起こりません。どの順に混ぜても、どちらかのストアは必ず先行するからです。ところが実機の x86 でこのコードを回すと 0,0 が観測されます。 つまり現実のハードウェアは SC ではありません。SC は理解とデバッグには最高ですが、各ストアの大域可視化を待つ実装はコストが高く、現代CPUは採用していません。
プログラマが暗黙に期待する「書いた順に、他スレッドからもそう見える」はまさに SC です。教科書のアルゴリズム(デッカー法、ピーターソン法)も SC を前提に正しさが証明されています。だから SC でないハードの上で素朴に組むと、証明済みのはずのアルゴリズムが壊れる——これがメモリモデルを学ぶ動機の核心です。
TSO・PSO・弱順序──ストアバッファが緩める階層
SC が崩れる根本原因は ストアバッファ です。コアは書き込みをいったん手元のバッファに溜めて次へ進み、後からキャッシュへ流します。すると「自分の書き込みは即読めるが、他コアには遅れて見える」非対称が生まれ、SC の大域全順序が破れます。どの並べ替えを許すかで、緩和モデルが段階化されます。並べ替えの4方向(LoadLoad / LoadStore / StoreStore / StoreLoad)の基礎は メモリオーダリングとメモリバリア を土台にしてください。
| モデル | 許す並べ替え | 代表アーキテクチャ | 強さ |
|---|---|---|---|
| SC | なし(プログラム順を全コアで保持) | 理論モデルのみ | 最強 |
| TSO(Total Store Order) | StoreLoad のみ | x86 / x86-64・SPARC | 強 |
| PSO(Partial Store Order) | StoreLoad + StoreStore | SPARC PSOモード(歴史的) | 中 |
| 弱順序(Weak Ordering) | ほぼ全方向(依存ロードは保持) | ARM・POWER・RISC-V | 弱 |
| リリース一貫性 | 同期操作で挟んだ区間だけ順序保証 | RCsc/RCpc・C++のacq/relの基礎 | 弱(明示同期) |
階段を一段ずつ降りると、解禁される並べ替えが増えていくのが見て取れます。
- TSO は StoreLoad だけを許します。各コアに 1本のFIFOストアバッファ があり、ストアはその順(StoreStoreは保持)でキャッシュへ出るが、後続ロードはバッファを飛び越して先に走れる、というモデルです。x86 が該当します。
- PSO はさらに StoreStore も 許します。ストアバッファがアドレスごとに分かれFIFOでなくなるイメージで、書き込み同士の順序すら保証されません。だから生産者が
dataを書いてからflagを立てても、消費者にはflagが先に見えうる。歴史的な SPARC PSO モードが代表です。 - 弱順序 は LoadLoad・LoadStore まで含め ほぼ全方向 を許します。唯一の歯止めは アドレス依存(あるロードの結果をアドレスに使う次のロードは順序保持)。ARM/POWER/RISC-V が該当し、アーキ別の詳細は メモリバリアの種類とアーキテクチャ別の振る舞い にまとめています。
x86(TSO) でも観測される「StoreLoad の追い越し」:
コア1: x=1; r1=y; ← x のストアがバッファに残る間に y を読む
コア2: y=1; r2=x; ← 同様
→ r1==0 かつ r2==0 が成立しうる(SCでは不可能)
リリース一貫性──「同期点だけ」順序を約束する
弱順序をそのまま使うのは過酷です。そこで実用モデルが リリース一貫性(Release Consistency, RC)。発想は「普通のメモリアクセスは好きに並べ替えてよい。ただし acquire/release と印を付けた同期操作の所だけ順序を守る」というものです。
- acquire(取得):この後ろの読み書きが、acquire より前へ出るのを禁止(後ろを止める片側の蓋)。
- release(解放):この前の読み書きが、release より後ろへ回るのを禁止(前を止める片側の蓋)。
スレッドB の release ストアを、スレッドA の acquire ロードが観測したなら、release 以前のBの全書き込みが acquire 以降のAから必ず見える。これが happens-before を確立する release-acquire の同期 で、ロックの取得が acquire、解放が release に対応します。重要なのは、片側だけ止める一方通行ゆえ全方向フェンスより軽く、弱順序機でも臨界区間の外側は自由に最適化できる 点です。
リリース一貫性にも2系統あります。同期操作どうしが SC のように全順序を持つ RCsc と、より緩い RCpc(プロセッサ整合性ベース)です。C++ の memory_order_seq_cst は RCsc 寄り、acquire/release 単体は RCpc 寄りの保証に対応します。ARMv8 の LDAR/STLR はRCsc的な強めの acquire/release を1命令で表す好例で、汎用の DMB より軽量です。
なぜ言語メモリモデルが要るのか
ここまではハードのモデルでした。しかしアプリ開発者がハードのモデルを直接相手にするのは破綻します。理由は3つ あり、これが C++11 や Java が独自の 言語メモリモデル を定めた動機です。
- 移植性:x86・ARM・POWER でモデルが違う以上、ハードに直接書いたコードは移植できません。言語が共通の抽象(acquire/release/seq_cst)を与え、コンパイラが各アーキの実機命令へ展開することで、1つのソースが全機で正しく動きます。
- コンパイラも並べ替える:CPUだけでなくコンパイラも最適化で命令を動かします。ハードのフェンスはCPUしか縛らないので、言語が「コンパイラとCPUの両方を縛る」契約点 を提供しないと、最適化で同期が壊れます。
- データ競合の定義:そもそも「何が壊れた状態か」を定義する言語が要ります。C++/Java は データ競合(data race) を「同期されない、少なくとも一方が書き込みの並行アクセス」と定め、それを起こすプログラムを 未定義動作(UB) と規定します。
その上で言語が与える最重要の保証が DRF-SC 定理 です。
Data-Race-Free(DRF)なプログラム、すなわち全ての共有データ競合を適切な同期(mutex やアトミック)で消したプログラムは、シーケンシャル一貫(SC)に振る舞う ことが保証されます。これが C++11/Java メモリモデルの中核です。意味は強烈で——正しく同期している限り、弱順序機の上でもプログラマは SC という直感的なモデルだけで考えてよい。緩和モデルの複雑さは、データ競合を含む(=バグのある)コードでだけ表面化します。だから「ロックやアトミックで競合を消す」ことが、性能と正しさを両立する唯一の正攻法なのです。
volatile が保証するのはアクセスの除去・融合を防ぐことだけで、他変数との順序もCPUの並べ替えも縛りません。スレッド間同期には使えず、必ず std::atomic(C11なら _Atomic)を使ってください。Java の volatile は acquire/release 相当の順序を持つ別物で、同名ゆえの混同が移植時の事故源になります。実装側の道具は カーネルのロックフリー同期とCAS も参照してください。
- 整合性モデル=アドレス間の可視順序の契約。コヒーレンシ(1アドレス内)とは別物。
- 強さの階層:SC > TSO(StoreLoadのみ許す) > PSO(StoreStoreも) > 弱順序(ほぼ全方向) > リリース一貫性(同期点だけ保証)。
- 緩和の源は ストアバッファ。x86=TSO、ARM/POWER/RISC-V=弱順序。
- 言語メモリモデルが要る理由は 移植性・コンパイラの並べ替え・データ競合の定義。DRF-SC:競合がなければ SC として考えてよい。
まとめ──弱いハードの上で、SC という直感を取り戻す道具
メモリ整合性モデルは「複数アドレスの読み書きにどの相対順序を許すか」の契約で、SC が最強(プログラム順のインターリーブのみ)、そこから TSO(StoreLoadのみ)→ PSO(StoreStoreも)→ 弱順序(ほぼ全方向)→ リリース一貫性(同期点だけ) と段階的に緩みます。緩和の正体は ストアバッファ による「自分の書き込みが他コアへ遅れて見える」非対称で、x86 は TSO、ARM/POWER/RISC-V は弱順序です。現実のハードが SC でない以上、移植可能で正しいコードを書くには 言語メモリモデル が不可欠で、acquire/release と データ競合 の定義を与え、DRF-SC 定理——競合さえ消せば SC として考えてよい——を約束します。だから普段は mutex やアトミックで競合を消すだけでよく、緩和モデルの細部は 自前のロックフリー構造を弱順序機へ載せるときだけ 立ち返れば十分です。土台のバリアは メモリオーダリングとメモリバリア、アーキ差は メモリバリアの種類とアーキテクチャ別の振る舞い へ。
OS Article
メモリ整合性モデルとシーケンシャル一貫性を実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
メモリ整合性モデル
比較で見る軸
難易度: advanced / カテゴリ: OS / タグ数: 5
導入後に効く点
緩和の源はストアバッファ。TSOはStoreLoadのみ、PSOはStoreStoreも、弱順序はほぼ全方向を許す。緩いほど速いが、同期点を自分でフェンスとして指定しないと観測順序が壊れる。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- OS
- タグ数
- 5
判断チェックリスト
- 自社の用途が「メモリ整合性モデル / シーケンシャル一貫性」に近いか確認する。
- 強みである「整合性モデルは『複数コアの読み書きにどの全順序を許すか』の契約。最強のシーケンシャル一貫性(SC)はプログラム順を保ったインターリーブだけを許し、TSO→PSO→弱順序→リリース一貫性と段階的に並べ替えを解禁していく。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。