KAME の STM は、ノード木の部分木を bundle / unbundle しながら楽観的にコミットする。
本デッキはその livelock-free (LL-free) 版(第 3 世代仕様
BundleUnbundle_2level_LLfree.tla)を解説する。中核アイデアは
「より古いトランザクションが勝つ」優先度調停 (privileged-TID negotiate):
CAS 競合時に古い側へ優先権を渡すことで retry 回数が構造的に有界になり、
TLC が CONSTRAINT なしで全状態を探索し尽くせる こと自体が livelock-free の形式証明になる。
priorityTag / CanProceed / PreemptTag / ClearMyTags) のみ。このあとの流れ: 記法と新出要素 → 検証アプローチの 3 世代 →
priorityTag → CanProceed → タグ更新規則 →
PreemptTag/ClearMyTags →
EventuallyAllDone(活性証明)→ 旧世代の意義と wait-free との区別。
BundleUnbundle_2level_LLfree.tla は元の 2-level spec に
privileged-TID negotiate 機構を追加した変種。
livelock-freeness を TLC で 形式的に 検証可能にすることが目的。
本ページは差分のみ — 共通部分は slides_layer2.html 参照。
本バリアントの位置付け:
KAME の TLA+ 仕様は有限性をどう確保するかで 3 世代に分かれる。
LLfree は最新の Gen 3 にあたり、CONSTRAINT に依存せず priority 機構で
構造的 に状態空間を有限化することで、TLC が実行する 完全 exhaustion
そのものが livelock-free 性の形式証明となる。実装の
m_link->negotiate() / release_privileged_tidstamp() を
spec 上に直接写している。
Privilege = FALSE で priority 機構をすべて無効化すると 状態空間が発散し TLC が終了しない — これが「privilege 機構は必須」を示す 最も直接的な反証である (末尾スライド「旧世代の保存意義」で詳述)。
| 記号 | 意味 | 定義 (要約) |
|---|---|---|
priorityTag[n] |
ノード n の privileged-TID タグ | Null | <<iter, tid>> per node |
iter(t) |
スレッド t の transaction 年齢 (派生量) | MaxCommits - iterBudget[t] |
MyTag(t) |
t の現在のタグ値 | <<iter(t), t>> |
TagOlder(a, b) |
a が b より古い (= 優先される) | (a[1], a[2]) の lex 順序で a が小 |
CanProceed(t, n) |
t が n で CAS 実行可能か | tag が Null または自分のタグ (C++ tag_as_contender() と同じ 2 条件) |
TagAfterFail(t, n) |
CAS 失敗後の priorityTag[n] | 3 段階のラダー (Null→mine, mine→refresh, older-wins 上書き) |
TagAfterSuccess(t, n) |
CAS 成功後の priorityTag[n] | priorityTag[n] ← 不変 (Transaction-scope persistence) |
ClearMyTags(t) |
コミット成功時の解放 (失敗時はタグ保持) | 全ノードで自分のタグ → Null (C++ finalizeCommitment→drop_tags) |
PreemptTag(t, n) |
新規 action: 古い t が若い tag を奪う | 独立アクション (CanProceed 経路と並行) |
PROPERTY EventuallyAllDone |
liveness check (新規) | <>AllDone — 全スレッド到達保証 |
| 項目 | 元 2level | LLfree |
|---|---|---|
| CONSTRAINT SerialBound | 必要 (自然数 serial が無限増加するため) | 不使用 (priorityTag 機構で有限性が構造保証) |
| SYMMETRY ThreadSymmetry | 適用 | 削除 (TLC が liveness で symmetry 警告; Threads は順序付き Nat が必要) |
| Threads の型 | model values {t1, t2} |
{1, 2} (Nat — TagOlder の a[2] < b[2] 型整合) |
| MaxSerial / MaxPayload | cfg で指定 | 廃止 — serial は Lamport 方式 (Nat; 演算子本体で未参照)、payload は単調 Nat カウンタ |
| DebugSerialBound | 未定義 | == TRUE (常に通過。Lamport serial は無界のため数値チェックを廃止) |
共通点 (元 spec から不変): ツリー構造 (Parent + Child1 + Child2)、4-phase bundle protocol、commit pipeline、 unbundle、coarse / fine / superfine 三段構成、safety INVARIANT (SnapshotConsistency, NoPriorityLoss, BundleRefConsistency, MissingPropagation, TerminalPayloadCheck) — これらは元 spec と完全同一。LLfree が追加するのは livelock-free 性を保証する gating 層のみ。
KAME の bundle/unbundle 系 TLA+ 仕様は、有限性をどう確保するかで 3 世代に分かれる。 LLfree は Gen 3 にあたり、本バリアントが CONSTRAINT なしで完全 exhaustion できることが livelock-free 性の構造的証明となる。
| 世代 | Serial | CONSTRAINT | Priority 機構 | TLC の動作 | 何を示すか |
|---|---|---|---|---|---|
| Gen 1 (modular) | % MaxSerial で wrap |
なし | なし | SerialWrapAround violation で落ちる |
privilege 機構なしの破綻を反例として示す |
| Gen 2 (Nat+CONSTRAINT) | Nat 単調増加 |
SerialBound で打ち切り |
なし | 終了するが探索打ち切り依存 | 制限付き探索内の safety のみ |
| Gen 3 (LLfree) | Lamport 方式 (unbounded Nat) | なし | 常時 ON (priorityTag) |
構造的有限が成立し exhaustion 完走、liveness PASS | safety + livelock-free を構造的に証明 |
2-level tree。bundle/unbundle protocol そのもの・4-phase / commit pipeline / coarse-fine-superfine 三段
構成は元 BundleUnbundle_2level.tla と 完全同一。LLfree が追加するのは
livelock-free 性を保証する gating 層のみ。
\* Variant of BundleUnbundle_2level that mirrors KAME's livelock-free
\* negotiate mechanism explicitly so TLC's exhaustive search reaches a
\* finite state space without modular-serial wraparound.
\*
\* Key differences from the base 2level spec:
\* - Lamport serial (TID encoded in lower digit, counter in upper).
\* No globalSerial — C++ has none. No MaxSerial constant.
\* - New variable priorityTag[n]: per-node (Null | <<iter, tid>>) tag.
\* - Set by a thread when its CAS fails at a "negotiate point";
\* others see the tag and only proceed if it's Null or matches mine.
\* - Older transactions (smaller iter, then smaller tid) win.
\* - Tag cleared at Transaction boundaries via ClearMyTags —
\* mirroring C++ release_privileged_tidstamp().
665,218 distinct states / depth 89 / 28 秒 / queue 0 (exhaustion)
BundleUnbundle_2level_LLfree_micro_mc.cfgMaxCommits = 1, |Threads| = 2, fine atomicityEventuallyAllDone == <>AllDone liveness PASS
BundleUnbundle_2level_LLfree_superfine_mc.cfg (Phase 1 prestamp CAS + Phase 3
DISTURBED 検出) でも同様に完走。C++ の最も忠実なミラーが動作することを示す。
priorityTag 変数と補助オペレータ
LLfree が導入する唯一の新変数 priorityTag[n] と、それを操作する補助オペレータ。
これらは全て C++ の m_priority_tidstamp / negotiate() を直接ミラーする。
VARIABLES
serial, linkage, pc, op, target, local,
iterBudget, childQueue,
priorityTag \* [Nodes -> Null | <>]
\* globalSerial は廃止 (C++ に対応物なし)。serial は Lamport 方式。
vars == <<serial, linkage, pc, op, target, local,
iterBudget, childQueue, priorityTag>>
\* Serial encoding: EncodeSerial(cnt, tid) = cnt * SerialBase + tid
\* SerialBase = 1 + |Threads| (> max TID)
Init ==
/\ ... \* 元 2level と同じ初期化
/\ serial = [t \in Threads |-> EncodeSerial(0, t)] \* TID 埋め込み
/\ priorityTag = [n \in Nodes |-> Null]
\* iter(t): 完了したフル iteration 数。t と組み合わせて
\* <> の全順序を作る (smaller = older = 優先)。
iter(t) == MaxCommits - iterBudget[t]
MyTag(t) == <<iter(t), t>>
\* a が b より older。(iter, tid) の lex 順序で a が小。
TagOlder(a, b) ==
\/ a[1] < b[1]
\/ (a[1] = b[1] /\ a[2] < b[2])
\* Lamport serial helpers (C++ transaction.h:547-576 に対応)
SerialBase == 1 + Cardinality(Threads)
SerialCounter(s) == s \div SerialBase
SerialTID(s) == s % SerialBase
EncodeSerial(cnt, tid) == cnt * SerialBase + tid
| オペレータ | 意味 | C++ 対応 |
|---|---|---|
priorityTag[n] |
ノード n の登録 tag (Null | <<iter, tid>>) |
Linkage::m_priority_tidstamp |
iter(t) |
派生量: 完了 iteration 数 (= 別変数を持たない) | now_us_tagged() Lamport stamp の粗い近似 |
MyTag(t) |
t が今登録すべきタグ値 | Transaction::priority_tidstamp() |
TagOlder(a, b) |
a が古い (= 競合時に勝つ) | C++ 側の lex 比較 (iter, tid) |
iter(t) が 派生量 である理由新変数を増やさずに iterBudget[t] から計算できるため、状態空間は 変わらない。
TLC は iter(t) を毎回 MaxCommits - iterBudget[t] と評価するだけで、
新しい状態軸を生まない。
結果: tag 機構を加えても主要な状態爆発は priorityTag(値の組み合わせ)のみ。
cfg からの制約: Threads = {1, 2} のように Nat で定義する必要がある。
TagOlder(a, b) 内の a[2] < b[2] で
Threads 要素同士を順序比較するため、Nat (整数) 型でなければ型整合しない。
元 2level の {t1, t2} (model values) は使えない。SYMMETRY ThreadSymmetry も 削除。Threads が順序付き Nat なので
`Permutations(Threads)` は意味を保てず、また TLC は liveness 検査と SYMMETRY の併用で
違反検出漏れを警告する。LLfree は exhaustion が高速なので SYMMETRY 不要。| 変数 | 取りうる値の数 |
|---|---|
priorityTag[n] (1ノード) |
1 + (MaxCommits+1) × |Threads| 値(= Null + すべての <<iter, tid>> ペア) |
| 全ノード (3 nodes = Parent + Child1 + Child2) | 上記の 3 乗 (独立) |
| micro cfg (MaxCommits=1, |Threads|=2) | 1 + 2×2 = 5 値 / ノード → 5³ = 125 通り (priorityTag 部分のみ) |
proof_semantics.md §1 によれば、micro cfg 全体での distinct states は 665,218。 priorityTag が掛け算で効くものの、retry 数を有界に保つことで他変数の爆発を抑え、 全体としては 有限 に収まる。
CanProceed(t, n) — CAS の前提条件
LLfree の中核 gate。元 spec のすべての CAS 試行に CanProceed(t, n) /\ ... が前置され、
登録 tag が許可しなければ その thread は進めない。これが priority 機構の中心。
\* CanProceed: gate for any CAS attempt at node n by thread t.
\* Matches C++ tag_as_contender(): a thread proceeds when:
\* (a) no privileged tidstamp registered, OR
\* (b) the registered tidstamp's tid is mine.
\* No zombie-tag check: tags are cleared only on commit success
\* (ClearMyTags), so inactive state is reached exclusively via success.
CanProceed(t, n) ==
LET tag == priorityTag[n] IN
\/ tag = Null
\/ tag /= Null /\ tag[2] = t
| 条件 | 意味 | C++ 対応 |
|---|---|---|
tag = Null |
誰も tag を登録していない — 自由参入 | m_priority_tidstamp.load() == 0 |
tag[2] = t |
自分の tag (any iter) — 同一トランザクションの再試行や以前のリトライの遺留 | == priority_tidstamp() (自分のスタンプと一致) |
旧版にあった ~ActiveThread(tag[2]) (zombie tag 判定) は削除。
タグはコミット成功時にのみ解放されるため、非アクティブ状態は必ず成功経路を経る
→ stale tag が残ることはなく、このケースは不要。
proof_semantics.md §8: 8/8 priority-gated CAS site で C++ 1:1 対応検証済み。 spec 側の主な call site は以下 10 箇所:
| アクション | ノード | 意味 |
|---|---|---|
BundlePhase1 (superfine prestamp) | Parent | 束ね開始時の serial bump |
BundlePhase2 | Parent | parent missing=TRUE で CAS |
BundlePhase3 coarse | 各 c \in Children | 全 child を一括 BundledRef 化 |
BundlePhase3 fine 成功 | 各 c | 1 child / step で BundledRef 化 |
BundlePhase3 fine 失敗 | 各 c | 失敗時の rollback + restart |
BundlePhase4 | Parent | missing=FALSE で最終 CAS |
CommitParent | Parent | parent payload commit |
CommitTryCAS | target[t] | child 直接 commit |
UnbundleCASAncestors | Parent | parent missing=TRUE への戻し |
UnbundleCASChild | target[t] | child を priority に復帰 |
\* CommitParent を例に
CommitParent(t) ==
/\ pc[t] = "commit_parent"
/\ ...
/\ CanProceed(t, Parent) \* ★ gate: tag が許可しない限り進めない
/\ LET ser == GenSerial(t, pw.serial) \* Lamport serial
IN
\/ \* CAS 成功
/\ ...
/\ priorityTag' = [priorityTag EXCEPT ![Parent] = TagAfterSuccess(t, Parent)]
/\ ...
\/ \* CAS 失敗
/\ ...
/\ priorityTag' = [priorityTag EXCEPT ![Parent] = TagAfterFail(t, Parent)]
/\ ...
全アクションで CanProceed を前置し、CAS の成否分岐ごとに
TagAfterSuccess / TagAfterFail で priorityTag を更新する。
これにより「進めない thread は登録だけして待つ」「進めた thread は tag を保持
(Transaction-scope persistence)」という C++ negotiate() 動作を表現する。
CanProceed(t, n) が FALSE の場合、そのアクションは enabled でない
(TLA+ の意味で「取れない」) → 当該 step では別の thread が動く。
これにより:
PreemptTag で若い tag を奪うことで膠着を打開 (後述の PreemptTag スライドで詳述)WF_vars(NextStep) の下で必ず古い thread が前進し、最終的に tag を release するproof_semantics.md §4 より直接引用: 「他スレッドは tag が Null か自分のものでなければ 待つ (CanProceed 否定)」「各時点で "優先される 1 スレッド" が必ずいる」「そのスレッドの retry 回数は他スレッドの数に対して 有界」「全スレッドの累積 retry 数も有界」「→ serial 増加が有界 → 状態空間が有限 → TLC 終了」。 この論証チェーンが livelock-free 性の構造的証明 (§2 の議論) の核。
TagAfterFail / TagAfterSuccess — タグ更新規則
CAS の成否ごとに priorityTag[n] をどう更新するかを定義する 2 つの関数。
失敗時は 4 段階のラダー、成功時は 不変 (Transaction-scope persistence)。
TagAfterFail(t, n) — CAS 失敗時のタグ更新\* TagAfterFail(t, n): the value priorityTag[n] should hold after thread t's
\* CAS at n fails. Mirrors C++ tag_as_contender():
\* - No tag (slot empty): register mine.
\* - My own tag: refresh to current MyTag(t).
\* - Other thread's tag, I'm older: overwrite with mine.
\* - Other thread's tag, I'm younger: keep theirs (defer to older).
TagAfterFail(t, n) ==
IF priorityTag[n] = Null
THEN MyTag(t)
ELSE IF priorityTag[n][2] = t
THEN MyTag(t)
ELSE IF TagOlder(MyTag(t), priorityTag[n])
THEN MyTag(t)
ELSE priorityTag[n]
| 条件 | 動作 | 意図 |
|---|---|---|
tag が Null |
MyTag(t) を登録 |
新規登録 (CAS が混んでいるシグナル) |
| tag は自分 (任意の iter) | MyTag(t) に更新 (refresh) |
iter が進んでいれば最新値に |
| holder が 自分より若い | MyTag(t) に上書き (older-wins) |
C++ tag_as_contender() と同じ: 古い tx が若い tag を奪う |
| holder が 自分より古い | そのまま (priorityTag[n] を維持) | 古い thread の登録を奪わない |
旧版にあった「holder が inactive (zombie)」ケースは削除。 タグはコミット成功時のみ解放され、非アクティブ = 必ず成功済み → zombie tag は存在しない。
TagAfterSuccess(t, n) — CAS 成功時の更新\* TagAfterSuccess(t, n): on CAS success at n, KEEP the tag — Transaction-
\* scope persistence (matches C++ ScopedNegotiateLinkage outer-scope which
\* releases only at scope end, not at each inner CAS). Per-CAS clearing
\* allows a peer Transaction to race in between CAS phases of the holding
\* Transaction and force endless re-bundling. The actual release happens
\* at Transaction boundaries via ClearMyTags below.
TagAfterSuccess(t, n) == priorityTag[n] \* ← 不変 (KEEP)
per-CAS clearing が livelock を引き起こす理由: 「CAS が成功するたびに自分の tag を Null に戻す」と、CAS フェーズの合間で 別 Transaction が tag のない隙に同じノードへ割り込むことができる。 その Thread が bundle/unbundle を起こすと、最初の Transaction は再び Phase 1 からやり直しになり、 この ping-pong が無限に繰り返される。
正しい設計: tag は CAS 単位で clear せず、コミット成功時
(CommitParent 成功 / CommitDone 成功) でのみ release する。
失敗・リトライ時はタグを保持したまま次の snapshot へ (C++ operator++ が
drop_tags_n_privilege() を呼ばない動作に対応)。
ScopedNegotiateLinkage との対応| C++ | TLA+ LLfree |
|---|---|
ScopedNegotiateLinkage tag(*link) in commit_xxx() outer scope |
SnapCheck / CommitParent / CommitTryCAS の前段で CanProceed 判定 |
m_priority_tidstamp.store(my_stamp) on CAS fail |
priorityTag' = [priorityTag EXCEPT ![n] = TagAfterFail(t, n)] |
| CAS 成功時はストア しない | TagAfterSuccess(t, n) == priorityTag[n] (no-op) |
scope 脱出時 (デストラクタ) で release_privileged_tidstamp() |
ClearMyTags(t) at Transaction-end(PreemptTag/ClearMyTags のスライド) |
失敗時に「holder が active で自分が若い」場合、priorityTag は変えない。
これは「古い thread が登録した tag を若い thread が誤って奪う」のを防ぐ。
ただし、若い thread が 先に tag を取った後で 古い thread が来た場合は逆転が必要 →
これを担うのが独立アクション PreemptTag (次スライド)。
不変条件:
任意のノード n で priorityTag[n] /= Null ならば、
登録された <<iter, tid>> はその瞬間で「最も古い (活性) thread の値」。
zombie tag はコミット成功経路でのみ非アクティブになるため構造的に存在しない。
これが livelock-free 性の局所不変条件 (proof_semantics §4)。
PreemptTag アクション + ClearMyTagsLLfree が追加する独立アクション (CAS と並行) と Transaction-end での tag 一括解放。 これらが揃って初めて「古い thread が永久 lockout されない」性質が成立する。
PreemptTag(t, n) — 独立アクション\* @c11_action PreemptTag(t, n):
\* An older thread can replace a younger thread's tag at a node, allowing
\* it to subsequently proceed via CanProceed. Mirrors C++
\* try_register_privileged_tidstamp() succeeding for an older Transaction.
\* Without this action, a younger thread that grabbed the tag first could
\* permanently lock out an older thread.
PreemptTag(t, n) ==
/\ priorityTag[n] /= Null
/\ priorityTag[n][2] /= t \* not already mine
/\ TagOlder(MyTag(t), priorityTag[n])
/\ priorityTag' = [priorityTag EXCEPT ![n] = MyTag(t)]
/\ UNCHANGED <<serial, linkage, pc, op, target,
local, iterBudget, childQueue>>
PreemptTag の 3 つの enabling 条件| 条件 | 意味 |
|---|---|
priorityTag[n] /= Null |
preempt 対象の tag が存在 (Null なら CAS 経路で取得できる) |
priorityTag[n][2] /= t |
自分の tag を preempt する意味なし |
TagOlder(MyTag(t), priorityTag[n]) |
自分の方が古い場合のみ preempt 可 |
旧版にあった ActiveThread(t) (自分が活性) と
ActiveThread(holder) (holder が活性) の 2 条件は削除。
zombie tag が存在しないため「inactive holder → CanProceed 経路で進める」
という分岐が不要になった。
シナリオ: 若い thread tyoung が CAS 失敗で tag を取った。直後に
古い thread told が登場、told の CAS は CanProceed 失敗で進めない。
TagAfterFail は CAS 試行をした側 しか tag を更新しない (CAS が enabled
でないのに Fail にもならないため)。
→ 独立アクション PreemptTag が CAS とは別経路で tag を奪取する必要がある。
これがないとどうなるか:
proof_semantics.md §4 に記述されているとおり、若い thread が tag を持ったまま
進めば古い thread の retry が無限になる可能性が出る。PreemptTag は
TLC の状態空間内で「古い thread が tag を奪う step」を必ず生成するため、
WF_vars(NextStep) の下で前進が保証される。
ClearMyTags(t) — Transaction-end での一括解放\* ClearMyTags(t): release ALL of thread t's tags at every node. Called only
\* on commit SUCCESS (CommitParent success, CommitDone success) — matches
\* C++ finalizeCommitment → drop_tags_n_privilege. On failure, tags are
\* preserved across retries (C++ operator++ does not call drop_tags).
ClearMyTags(t) ==
[n \in Nodes |->
IF priorityTag[n] = Null
THEN priorityTag[n]
ELSE IF priorityTag[n][2] = t
THEN Null
ELSE priorityTag[n]]
| アクション | priorityTag 更新 | 意味 |
|---|---|---|
CommitParent 成功 |
ClearMyTags(t) |
commit 完了 → 全 tag 解放 (C++ finalizeCommitment) |
CommitParent 失敗 |
[priorityTag EXCEPT ![Parent] = TagAfterFail(t, Parent)] |
タグ保持のままリトライ; Parent tag のみ更新 (C++ operator++) |
CommitDone 成功 |
ClearMyTags(t) |
per-child commit 終了 → 全 tag 解放 |
CommitDone 失敗 |
UNCHANGED priorityTag |
タグ保持のままリトライ (C++ operator++) |
| C++ | TLA+ LLfree |
|---|---|
try_register_privileged_tidstamp(my_stamp) succeeding for older transaction |
PreemptTag(t, n) |
finalizeCommitment() → drop_tags_n_privilege() (commit success) |
ClearMyTags(t) at CommitParent success / CommitDone success |
~Transaction() → drop_tags_n_privilege() (abort/RAII) |
TLA+ では Transaction は明示的に終了する設計のため対応物なし |
operator++() — drop_tags NOT called on retry |
CommitParent/CommitDone 失敗: UNCHANGED priorityTag or EXCEPT |
proof_semantics.md §4 「older-wins 規律」直接引用:
これが PreemptTag + ClearMyTags の存在で初めて成立する。
両者を欠くと、若い thread の lockout または stale tag による前進阻害で
論証チェーンが破綻する。
EventuallyAllDone PROPERTY と構造的 LL-free 性LLfree が形式検証で示すべきものは何か。proof_semantics.md §2 の論証チェーンが livelock-free 性の証明である理由を確認する。
\* AllDone: terminal condition — every thread has consumed its full
\* iteration budget and returned to idle.
AllDone == \A t \in Threads : pc[t] = "idle" /\ iterBudget[t] = 0
\* NextStep: every action that can make real progress.
NextStep ==
\E t \in Threads :
\/ SnapRead(t) \/ SnapCheck(t)
\/ BundlePhase1(t) \/ ... \/ BundlePhase4(t)
\/ CommitParent(t) \/ ... \/ CommitDone(t)
\/ UnbundleWalk(t) \/ ... \/ UnbundleCASChild(t)
\/ \E n \in Nodes : PreemptTag(t, n) \* ★ tag preempt も progress
\* Terminating: self-loop at AllDone — deadlock detection を有効のまま
\* 真の deadlock を見逃さないためのイディオム (proof_semantics §6)
Terminating ==
/\ AllDone
/\ UNCHANGED vars
Next == NextStep \/ Terminating
Spec == Init /\ [][Next]_vars /\ WF_vars(NextStep)
\* ↑ Terminating は WF 外。さもなくば
\* "永遠にスタッターすればよい" になり
\* 活性が無意味化 (proof_semantics §6)
EventuallyAllDone PROPERTY\* EventuallyAllDone: under WF, every execution eventually reaches AllDone.
\* This is the key LL-free property — if priority gating is correct, no
\* thread can be starved indefinitely, so all threads eventually finish.
EventuallyAllDone == <>AllDone
TLA+ の <>P は「P が いつか 真になる」(eventually)。WF_vars(NextStep)
で「enabled な NextStep 遷移は永遠に無視されない」を仮定すれば、priority 機構が正しい限り
全スレッドが必ず AllDone に到達することを TLC が verify する。
「終了 + CONSTRAINT なし + Nat serial 単調増加 → livelock-free 自動」
| (1) | TLC が exhaustion 完走 (queue 0) ⇒ 状態空間が 真に有限であることの証明 (CONSTRAINT で打ち切ったのではなく、全到達状態を列挙し終えた) |
| (2) | Serial が Nat で単調増加 (modular wrap なし) |
| (3) | livelock 候補となる全アクション (retry を含む) が必ず serial を bump する |
| (4) | 状態グラフ上のサイクル ⇒ 同じ状態に戻る ⇒ serial も同じ値に戻る ⇒ 単調増加と矛盾 ⇒ サイクル不可能 |
| (5) | サイクルなし ⇒ 無限挙動が存在しない ⇒ livelock-free |
この論証で、EventuallyAllDone が CONSTRAINT 依存ではなく
構造的 に成立することが示される。priority 機構が正しい設計であれば、
retry 回数が有界 → serial 有界 → 状態空間有限 → 上記論証が走る。
| 元 2level | LLfree | |
|---|---|---|
| CONSTRAINT | SerialBound (探索打ち切り) |
なし — 状態空間有限性は priorityTag が保証 |
| SYMMETRY | ThreadSymmetry |
削除 (Threads は Nat / liveness 検査と非両立) |
| Threads | {t1, t2} (model values) |
{1, 2} (Nat — TagOlder の型整合) |
| MaxSerial / MaxPayload | cfg で指定 | 廃止 |
| INVARIANT | Safety 系 5 つ | 同じ Safety 系 + TerminalPayloadCheck + QuiescentCheck |
| PROPERTY | なし | EventuallyAllDone == <>AllDone (新規) |
| CHECK_DEADLOCK | -deadlock flag で抑制 |
デフォルト TRUE (Terminating disjunct で正規終端を吸収) |
| cfg | Threads | States | Depth | Time | 結果 |
|---|---|---|---|---|---|
| coarse micro (MaxCommits=1) | 2 | 665,218 | 89 | 28s | Pass + liveness (laptop) |
| superfine | 2 | 2,676,196 | 129 | 3:12 | Pass + liveness |
| superfine confC (all-root) | 3 | 137,333,348 | 96 | 6:35 | Pass (ohtaka) |
| MaxCommits=2 superfine | 2 | 127,586,599 | 311 | 4:40 | Pass (ohtaka) |
| dynamic release superfine live | 2 | 413,884,516 | 320 | 7:13 | Pass + liveness (ohtaka) |
結論: 全構成で
safety + livelock-free 性が構造的に証明された。詳細: kamestm/tests/VERIFICATION.md §3、doc/verification_log.md。
| 仕様 | 違い | 解説 |
|---|---|---|
BundleUnbundle_3level_LLfree.tla | Grand → Parent → Child 3 段木、再帰的 inner bundle、多段 unbundle walk | slides_layer2_LLfree_3level.html |
BundleUnbundle_2level_LLfree_dynamic.tlaBundleUnbundle_3level_LLfree_dynamic.tla | オンライン insert(online=true) / release を追加。InsertThreads / ReleaseThreads 等で role 構成可能 | slides_layer2_LLfree_dynamic.html |
BundleUnbundle_hardlink_*.tla (5 仕様) | 2-parent / 1-child の hard-link topology、Phase 4 reachability gate、bug repro + fix simulation、per-action fairness | slides_hardlink.html |
LLfree が完成したからといって旧世代仕様を消去すべきではない理由 (proof_semantics §5)、 および「livelock-free」と「wait-free」の混同を防ぐ整理 (proof_semantics §7)。
SerialWrapAround violation で落ちるModGT 比較が破綻する意義: 「LL-free 機構が必要であること」自体を violation として証明している 貴重な反例。Gen 3 がなぜ必要かの最も説得力ある根拠 (proof_semantics §5)。
SerialBound) を外すと状態数が発散意義: Gen 3 への動機を明示する役割。CONSTRAINT 付きの終了は「打ち切られた領域に 入る前の safety」しか保証せず、liveness は出ない (proof_semantics §3)。
| 世代 | 仕様 file | 動作 | 証明できるもの |
|---|---|---|---|
| Gen 1 (modular) | (古い BundleUnbundle_2level.tla) |
SerialWrapAround violation |
「priority 機構なしでは破綻する」を動的に |
| Gen 2 (Nat+CONSTRAINT) | 現 BundleUnbundle_2level.tla |
終了するが状態数発散傾向 | 制限付き探索内の safety |
| Gen 3 (LLfree) | BundleUnbundle_2level_LLfree.tla |
CONSTRAINT なし完全 exhaustion | safety + livelock-free 性 (構造的) |
3-level spec (BundleUnbundle_3level_LLfree.tla) では Privilege が
明示的な CONSTANT として存在する。Privilege=FALSE に設定すると priority 機構が
すべて無効化され、元の non-LL-free 挙動と等価になる。
Privilege=TRUE (LLfree) | Privilege=FALSE (non-LL-free) | |
|---|---|---|
CanProceed(t, n) |
tag に基づく gate — 若い thread を止める | 常に TRUE — gate なし |
TagAfterFail(t, n) |
4 段階ラダーで tag を登録/更新 | no-op — tag は変わらない |
ClearMyTags(t) |
Transaction 境界で自分の tag を Null に | no-op |
PreemptTag(t, n) |
独立 action として有効 | disabled (条件が成立しない) |
| 状態空間 | 有限 → TLC exhaustion 完走 | 発散 — retry が無限に増え TLC が終了しない |
EventuallyAllDone |
PASS | violation (livelock 存在) |
Privilege=FALSE で実際に TLC を動かすと状態空間が発散することで、
「priority 機構は必須であり、単に Nat serial に切り替えるだけでは LL-free にならない」
ことが 直接 確認できる。
| Lock-free / LL-free (KAME) | Wait-free | |
|---|---|---|
| 定義 | fairness 仮定下で全スレッドが eventually 進む | 他スレッドに依存せず有界ステップ数で完了 |
| fairness 依存 | WF の下でのみ活性が出る | fairness 非依存 |
| CAS retry ベース | OK (優先スレッドが進む間、他は有界回数 retry) | 本質的に出ない |
| TLA+ で示すには | <>AllDone + WF で十分 |
スレッドごとの有界ステップ数 invariant が必要 |
KAME が示すのは lock-free + livelock-free。 wait-free は CAS retry 設計の本質から成立しない (設計通り)。
| 性質 | 形式検証 | C++ 実装 |
|---|---|---|
| Safety | CONSTRAINT なし完全 exhaustion で証明 | 大規模ストレステストで未破綻 |
| LL-free | EventuallyAllDone PASS (+ Privilege=FALSE で発散確認) |
livelock 観測なし |
| Wait-free | 証明する設計でない | CAS retry ベースなので本質的に出ない |
BundleUnbundle_2level_LLfree.tla /
BundleUnbundle_3level_LLfree.tla |
元 spec の解説 → slides_layer2.html