| 記法 | 意味 | 例 |
|---|---|---|
== | 定義(「〜とは」) | Init == ... は「Initとは...のこと」 |
/\ | AND(論理積) | A /\ B は「AかつB」 |
\/ | OR(論理和) | A \/ B は「AまたはB」 |
=> | 含意(「ならば」) | A => B は「AならばB」 |
~ | NOT(否定) | ~A は「Aでない」 |
\in | 集合の要素 | x \in S は「xはSの要素」 |
\notin | 集合の非要素 | x \notin S |
\cup | 和集合 | S \cup T |
\subseteq | 部分集合 | S \subseteq T |
\A | 全称量化子(∀, for all) | \A t \in Threads : P(t) |
\E | 存在量化子(∃, exists) | \E o \in Objects : ... |
| 記法 | 意味 | 例 |
|---|---|---|
VARIABLES x, y | 状態変数の宣言 | モデルの状態を構成する |
x' | 次の状態でのxの値 | x' = x + 1 は「xを1増やす」 |
UNCHANGED <<x, y>> | x, yは変化しない | このアクションでは変更なし |
Init | 初期状態の定義 | 全変数の初期値を指定 |
Next | 次状態関係(遷移) | 取りうるアクションの論理和 |
[][Next]_vars | 常にNextかstuttering | 仕様の時相論理式 |
WF_vars(Next) | 弱公平性(いつか実行される) | 活性のために必要 |
| 記法 | 意味 | 例 |
|---|---|---|
[x \in S |-> e] | 関数の定義(S上でeを返す) | [t \in Threads |-> 0] は全スレッド0で初期化 |
f[x] | 関数fのxでの値 | global_rc[o] はオブジェクトoのRC |
[f EXCEPT ![x] = e] | fのx番目だけeに変えた新関数 | [pc EXCEPT ![t] = "done"] |
@ | EXCEPT内で「現在の値」 | [f EXCEPT ![x] = @ + 1] は+1 |
[a |-> 1, b |-> 2] | レコード(構造体) | r.a でフィールドアクセス |
| 記法 | 意味 | 例 |
|---|---|---|
IF P THEN A ELSE B | 条件式 | 通常のif-then-else |
CASE p1 -> e1 [] p2 -> e2 | パターンマッチ | p1が真ならe1、p2が真ならe2 |
LET x == e IN ... | 局所定義 | C++のauto x = e;に相当 |
CHOOSE x \in S : P(x) | Pを満たすSの要素を1つ選ぶ | 非決定的選択 |
| 記法 | 意味 | 例 |
|---|---|---|
CONSTANTS | モデルパラメータ | Threads, Objects, MaxLocalRC |
0..N | 0からNまでの整数集合 | 0..MaxVal |
Nat | 自然数集合 | 型チェック用 |
BOOLEAN | {TRUE, FALSE} | 型チェック用 |
TLA+仕様のCONSTANTSは、モデルのパラメータを定義する。具体的な値は
.cfgファイル(TLCの設定ファイル)で指定する。
| 仕様 (.tla) | 設定 (.cfg) | 意味 |
|---|---|---|
CONSTANTS Threads, Objects | Threads = {t1, t2} | モデルチェック時の具体的な値を設定 |
CONSTANTS EnableCAS, EnableRecycle | EnableCAS = TRUE | 機能のON/OFFフラグ |
スレッド数・オブジェクト数を増やすと状態空間が指数的に増大する。
通常は2〜3スレッド、2オブジェクトで検証し、対称性(SYMMETRY)で縮約する。
| 記法 | 場所 | 意味 |
|---|---|---|
CONSTRAINT | .cfgファイル | この条件を満たさない状態に到達したら、その先の探索を打ち切る(枝刈り)。状態空間を有限にするために使う。安全性の検証ではない。 |
INVARIANT | .cfgファイル | 到達したすべての状態でこの条件が成り立つことを検証する。違反があればバグとして報告。 |
重要な違い:
CONSTRAINTは「この範囲だけ調べる」(探索の制限)。
INVARIANTは「これが常に成り立つか?」(安全性の検証)。
例: global_rc[o] <= 5 をCONSTRAINTに書くと、global_rcが5を超える状態は探索されない(見逃す可能性)。
同じ式をINVARIANTに書くと、global_rcが5を超えたらエラーとして報告される。
本モデルではStateConstraintを使用しない。
C++のスコープ破棄やモジュラー算術により状態空間を構造的に有限にし、
人為的な枝刈りなしで全到達可能状態を探索する。
{a, b, c}{"scan", "cas", "idle"}読み方のコツ: TLA+のアクション(例: ReserveScanCAS(t))は
「この条件が成り立つとき、変数をこう変える」という宣言。
/\で並んだ条件がすべて真のときに実行可能。
x'が次の状態、UNCHANGEDは変えないもの。
2層アーキテクチャにおける本層の位置づけ: 本スライドはかつて「Layer 0」と呼ばれていたが(ファイル名は slides_layer1.html に移動済み)、 論文の2層モデル(Layer 1: atomic_shared_ptr / Layer 2: bundle/unbundle + commit)に合わせてLayer 1に改称。 内容は変更なし。
kame/atomic_smart_ptr.h の参照カウントプロトコル全体。
C++のロックフリー atomic_shared_ptr を TLA+ で網羅的に検証する。
| 要素 | TLA+での表現 | C++での対応 |
|---|---|---|
| 共有ポインタ | ptr + local_rc | m_ref (uintptr_t) |
| オブジェクト | Objects (有限集合) | Ref構造体 |
| 参照カウント | global_rc[o] | Ref内のatomic<int> |
| 解放状態 | freed[o] | delete呼び出し |
| スレッド状態 | pc[t], thr_* | 関数内のローカル変数 |
VARIABLES
ptr, \* ポインタ(Object or NULL)
local_rc, \* ローカル参照カウンタ(下位3ビット相当)
global_rc, \* global_rc[o]: 各オブジェクトのグローバルRC
freed, \* freed[o]: 解放済みフラグ
pc, \* 各スレッドのプログラムカウンタ(22状態)
thr_op, \* "scan", "cas", "idle"
thr_pref, \* acquire_tag_ref_で読んだポインタ
thr_rcnt, \* acquire_tag_ref_で読んだローカルRC
thr_old, \* CAS: 期待する旧オブジェクト
thr_new, \* CAS: 新オブジェクト
thr_holds, \* local_shared_ptrで保持中のオブジェクト集合
thr_ls_ctx \* release_tag_ref_の戻り先
C++のm_ref.load()は単一アトミック操作であり、ポインタとローカルRCは
常に同じload値から導出される。TLA+でもこれを単一ステップでモデル化する。
(* アトミック読み取り: ptrとlocal_rcを同時に読む *)
ReserveScanRead(t) ==
/\ pc[t] = "rs_read"
/\ thr_pref' = [thr_pref EXCEPT ![t] = ptr] \* ← ptrを読む
/\ thr_rcnt' = [thr_rcnt EXCEPT ![t] = local_rc] \* ← local_rcを読む(同時)
/\ pc' = [pc EXCEPT ![t] = "rs_cas"]
(* CAS — ポインタとRCの両方が一致する場合のみ成功 *)
ReserveScanCAS(t) ==
/\ pc[t] = "rs_cas"
/\ thr_pref[t] /= NULL
/\ \/ (* CAS成功: ptr AND local_rc の両方が一致 *)
/\ ptr = thr_pref[t]
/\ local_rc = thr_rcnt[t]
/\ local_rc' = thr_rcnt[t] + 1 \* ピン留め!
\/ (* CAS失敗: 読み取りに戻ってリトライ *)
/\ ~(ptr = thr_pref[t] /\ local_rc = thr_rcnt[t])
/\ pc' = [pc EXCEPT ![t] = "rs_read"]
ポイント: 読み取りからCASまでの間に他スレッドがptr/local_rcを変更しても、 CASがフルワード比較(ptr + local_rc の両方)するため、不整合は必ず検出される。
(* Step 1: acquire_tag_ref_ → アトミック読み取り + CASでローカルRCを+1 [上のスライド参照] *)
(* Step 2: グローバルRCを+1 — relaxedで安全 *)
ScanIncGlobal(t) ==
/\ pc[t] = "scan_inc"
/\ global_rc' = [global_rc EXCEPT ![thr_pref[t]] = @ + 1]
/\ pc' = [pc EXCEPT ![t] = "scan_leave"]
(* Step 3: release_tag_ref_ → ローカルRCを-1 *)
LeaveScanCAS(t) ==
\/ (* CAS成功: ptrが変わっていない → ローカルRCを-1 *)
/\ ptr = thr_pref[t] /\ local_rc = thr_rcnt[t]
/\ local_rc' = thr_rcnt[t] - 1
\/ (* CAS失敗, ptrが同じ → リトライ *)
/\ ptr = thr_pref[t]
/\ pc' = [pc EXCEPT ![t] = "ls_read"]
\/ (* CAS失敗, ptrが変わった → フォールバック: global_rc-- *)
/\ ptr /= thr_pref[t]
/\ pc' = [pc EXCEPT ![t] = "ls_global"]
(* フォールバック: 入れ替えたスレッドがローカルRC分を転送済み *)
LeaveScanGlobal(t) ==
/\ global_rc' = [global_rc EXCEPT ![o] = @ - 1]
/\ IF global_rc[o] = 1
THEN freed' = [freed EXCEPT ![o] = TRUE] \* → 解放!
このフォールバックが不可欠: なしでは参照カウンタリーク or use-after-free。TLA+検証で確認。
| Phase | TLA+ Action | 動作 |
|---|---|---|
| 1 | CASPreInc | 新オブジェクトのglobal_rc++ |
| 2 | CASReserve→rs_* | acquire_tag_ref_でピン留め |
| 3 | CASCheck | pref != oldrなら失敗を返す |
| 4 | CASTransfer | ローカルRC分をglobal_rcに転送 |
| 5 | CASSwap | CAS (acq_rel): ポインタ入れ替え |
| 6 | CASCleanup | 旧オブジェクトのglobal_rc-- |
(* Phase 4: ローカルRC分をグローバルに転送 *)
CASTransfer(t) ==
/\ IF thr_pref[t] /= NULL /\ thr_rcnt[t] /= 1
THEN global_rc' = [global_rc EXCEPT
![thr_pref[t]] = @ + thr_rcnt[t] - 1]
(* Phase 5: CAS — フルワード比較 *)
CASSwap(t) ==
\/ /\ ptr = thr_pref[t] /\ local_rc = thr_rcnt[t] \* 一致
/\ ptr' = thr_new[t]
/\ local_rc' = 0
\/ /\ ~(...) \* CAS失敗 → Undo → release_tag_ref_ → リトライ
ABAとの関係: 呼び出し元はlocal_shared_ptrでoldrを保持
→ global_rc > 0 → 解放されない → ABAは構造的に不可能。
ただしload_shared_()ではスレッドが参照を持たないため、TLA+ではABAリサイクルを
sound over-approximationとしてモデル化。
CASとの違い: swapは旧ポインタを確認せず無条件に入れ替える。
PreIncフェーズ不要(所有権の交換であり追加ではない)。
StartSwap(t) ==
/\ EnableSwap
/\ pc[t] = "idle"
/\ \E newObj \in Objects \cup {NULL} :
/\ (newObj /= NULL => thr_holds[t][newObj] > 0)
/\ thr_new' = [thr_new EXCEPT ![t] = newObj]
/\ pc' = [pc EXCEPT ![t] = "cas_acquire"] \* PreIncをスキップ
/\ thr_op' = [thr_op EXCEPT ![t] = "swap"]
\* acquire_tag_ref_ 成功後:
\* CASTransfer: global_rc[pref] += local_rc(タグ→グローバル転送)
\* CASSwap: CAS ptr from pref to newObj, local_rc → 0
\* CASCleanup不要(PreIncしていない)
Reset(t) ==
/\ pc[t] \in {"idle", "done"}
/\ \E o \in Objects :
/\ thr_holds[t][o] > 0
/\ thr_holds' = [thr_holds EXCEPT ![t][o] = @ - 1]
/\ global_rc' = [global_rc EXCEPT ![o] = @ - 1]
\* decAndTest: global_rcが0に達したら解放
/\ freed' = [freed EXCEPT ![o] =
IF global_rc[o] = 1 THEN TRUE ELSE @]
swapperがポインタを入れ替えている最中にloaderが
acquire_tag_ref_を実行中の場合、loaderのrelease_tag_ref_は
ポインタ変更を検知しglobal_rcデクリメントにフォールバックする。
swapperのCASTransferが残存local_rcをglobal_rcに
転送することでバランスが取れる。このタグ→グローバル転送パスは
atomic_shared_ptrの正しさの核心であり、
swap と load が並行実行されて初めて行使される。
Recycle(t) ==
/\ EnableRecycle \* フラグで有効/無効を制御
/\ pc[t] \in {"idle", "done"}
/\ \E o \in Objects :
/\ freed[o] = TRUE \* 解放済みオブジェクトを
/\ global_rc[o] = 0
/\ freed' = [freed EXCEPT ![o] = FALSE] \* 再生!
/\ global_rc' = [global_rc EXCEPT ![o] = 1]
/\ thr_holds' = [thr_holds EXCEPT ![t] = @ \cup {o}]
Sound over-approximation: 全操作でリサイクルを許可(実際はcompareAndSwap_では不可能)。
過剰近似の下で安全性が証明されれば、実際のプログラムでも安全。
経緯: 著者は安全性を自明と判断。AIが過剰近似としてモデル化を提案、著者が同意。
| # | 不変条件 | 意味 |
|---|---|---|
| 1 | MemorySafety | ピン留め中のオブジェクトは解放されない |
| 2 | NoUseAfterFree | thr_holdsに含まれるオブジェクトは解放されない |
| 3 | GlobalRCNonNeg | 生存オブジェクトのglobal_rc ≥ 0 |
| 4 | FreedImpliesZeroRC | 解放済みならglobal_rc = 0 |
| 5 | InstalledNotFreed | ptrが指すオブジェクトは解放されない |
| 6 | LocalRCBounded | local_rc ≤ スレッド数 |
| 7 | TypeOK | 型不変条件 |
MemorySafety ==
\A t \in Threads :
(pc[t] \in {"scan_inc", "scan_leave",
"ls_read", "ls_cas", "ls_global",
"cas_check", ..., "cas_cleanup"}
/\ thr_pref[t] /= NULL)
=> freed[thr_pref[t]] = FALSE
| 構成 | スレッド | 状態数 | 時間 | 結果 |
|---|---|---|---|---|
| scan only | 3 | 40,934 | 1秒 | ✅ Pass |
| scan+CAS (ABAなし) | 2 | 3,549,304 | 12秒 | ✅ Pass (完全) |
| full (CAS+ABA) | 2 | 8,719,367 | 28秒 | ✅ Pass (完全) |
| swap+load (ABAなし) | 2 | — | — | ohtaka実行中 |
| swap+CAS+ABA | 2 | 6.6×10⁹+ | 18h+ | 部分探索、違反なし (I/O律速) |
| TLA+ 変数 | C++ (atomic_smart_ptr.h) | 生成C11 (test_atomic_shared_ptr.c) |
|---|---|---|
ptr + local_rc | m_ref (uintptr_t)上位=ptr, 下位=tag | _Atomic(uintptr_t) g_refget_ptr() / get_tag() |
global_rc[o] | o->refcnt (intrusive atomic) | _Atomic(uintptr_t) obj_refcnt[NUM_OBJECTS] |
freed[o] | delete呼び出し | int obj_freed[NUM_OBJECTS] |
thr_pref[t] | Ref* pref (local) | uintptr_t pref (local) |
thr_rcnt[t] | Refcnt rcnt (local) | unsigned rcnt (local) |
thr_holds[t][o] | local_shared_ptrの生存数 | thread-local hold index |
| TLA+ アクション | C++ 関数 | 生成C11 関数 | memory_order |
|---|---|---|---|
AcquireTagRefRead+ AcquireTagRefCAS | acquire_tag_ref_() | acquire_tag_ref() | relaxed → acq_rel |
LoadSharedIncGlobal | load_shared_()内 | load_shared()内fetch_add(refcnt, 1) | relaxed |
ReleaseTagRefCAS/ ReleaseTagRefGlobal | release_tag_ref_() | release_tag_ref() | acq_rel / acq_rel |
CASPreInc → CASSwap→ CASCleanup | compareAndSwap_() | compare_and_swap() | relaxed / acq_rel |
StartSwap → CASTransfer→ CASSwap | swap() | swap_exchange() | acq_rel |
Reset | local_shared_ptr::reset() | local_reset() | acq_rel (decAndTest) |
検証の三角形:
C++実装(著者が書いた)← 著者が独立に照合 → TLA+仕様
TLA+ → 生成C11(C++を参照せず、独立セッションで生成)→ GenMC検証(RC11)
この三角形が閉じることで、TLA+モデルの忠実性が確認可能。