▲ 俯瞰

Layer 2: LL-free 仕様

STM bundle/unbundle プロトコルの livelock-free 化 — 形式証明つき

KAME の STM は、ノード木の部分木を bundle / unbundle しながら楽観的にコミットする。 本デッキはその Layer 2 仕様 BundleUnbundle_2level_LLfree.tla を解説する。中核アイデアは 「より古いトランザクションが勝つ」優先度調停 (privileged-TID negotiate): CAS 競合時に古い側へ優先権を渡すことで retry 回数が構造的に有界になり、 TLC が CONSTRAINT なしで全状態を探索し尽くせる こと自体が livelock-free の形式証明になる。

このデッキの位置づけ

このあとの流れ: 記法と中核要素 → 仕様と検証結果 → priorityTagCanProceed → タグ更新規則 → PreemptTag/ClearMyTagsEventuallyAllDone(活性証明)→ wait-free との区別。

記法と中核要素

BundleUnbundle_2level_LLfree.tlaprivileged-TID negotiate 機構を備えた 2-level 仕様。 livelock-freeness を TLC で 形式的に 検証可能にすることが目的。 bundle/unbundle のツリー構造・プロトコル詳細は slides_layer2.html も参照。

核心となる性質: LLfree は CONSTRAINT に依存せず priority 機構で 構造的 に状態空間を有限化する。これにより、TLC が実行する 完全 exhaustion そのものが livelock-free 性の形式証明となる。実装の per-linkage Linkage::m_transaction_started_time スロット上の tag_as_contender() / drop_tags_n_privilege() を spec 上に直接写している。

Privilege = FALSE で priority 機構をすべて無効化すると 状態空間が発散し TLC が終了しない — これが「priority 機構は必須」を示す 最も直接的な検証である (末尾スライドで詳述)。

新規追加 — variables, operators, actions, properties

記号意味定義 (要約)
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 または自分のタグ。(spec は任意の他タグで hard-gate; C++ 対応物は advisory な fair_mode_blocks_me で Reserved スタンプのみ hard-block — spec の方が意図的に厳格。)
TagAfterFail(t, n) CAS 失敗後の priorityTag[n] 4 分岐のラダー (Null→mine, mine→refresh, older-wins 上書き, younger→維持)
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 — 全スレッド到達保証

有限性まわりの構成

項目LLfree での扱い
CONSTRAINT SerialBound 不使用 (priorityTag 機構で有限性が構造保証)
SYMMETRY ThreadSymmetry 不使用 (TLC が liveness で symmetry 警告; Threads は順序付き Nat が必要)
Threads の型 {1, 2} (Nat — TagOlder の a[2] < b[2] 型整合)
MaxSerial / MaxPayload 未使用 — serial は Lamport 方式 (Nat; 演算子本体で未参照)、payload は単調 Nat カウンタ
DebugSerialBound == TRUE (常に通過。Lamport serial は無界のため数値チェックなし)

safety まわりの構成: ツリー構造 (Parent + Child1 + Child2)、4-phase bundle protocol、commit pipeline、 unbundle、coarse / fine / superfine 三段構成、safety INVARIANT (SnapshotConsistency, NoPriorityLoss, BundleRefConsistency, MissingPropagation, TerminalPayloadCheck) を備える。これらに加え LLfree は livelock-free 性を保証する gating 層を持つ。

Overview: 仕様と検証結果

LLfree は CONSTRAINT なしで完全 exhaustion できることが livelock-free 性の構造的証明となる。ツリー構造・プロトコル本体に priority 機構 (priorityTag) を載せた構成を確認する。

ツリー構造

    Parent
      ├── Child1
      └── Child2

2-level tree。bundle/unbundle protocol 本体・4-phase / commit pipeline / coarse-fine-superfine 三段構成に加え、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++ drop_tags_n_privilege() (per-linkage default).

TLC verification の結果

867,696 distinct states / depth 89 / ~35 秒 / queue 0 (exhaustion)

superfine 構成でも完走

BundleUnbundle_2level_LLfree_superfine_mc.cfg (Phase 1 prestamp CAS + Phase 3 DISTURBED 検出) でも同様に完走。C++ の最も忠実なミラーが動作することを示す。

priorityTag 変数と補助オペレータ

LLfree が導入する唯一の新変数 priorityTag[n] と、それを操作する補助オペレータ。 これらは C++ の per-linkage Linkage::m_transaction_started_time / tag_as_contender()per-iteration 粒度でミラーする (iter は work-done カウンタで C++ の µs スタンプそのものではない=健全な粗視化であり順序同型ではない)。

変数宣言と Init

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 ==
    /\ ...  \* ツリー / linkage / pc の初期化
    /\ 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++ SerialGenerator, transaction.h:816-844 に対応)
SerialBase == 1 + Cardinality(Threads)
SerialCounter(s) == s \div SerialBase
SerialTID(s)     == s % SerialBase
EncodeSerial(cnt, tid) == cnt * SerialBase + tid

各オペレータの意味と C++ 対応

オペレータ意味C++ 対応
priorityTag[n] ノード n の登録 tag (Null | <<iter, tid>>) Linkage::m_transaction_started_time (per-linkage の atomic スロット)
iter(t) 派生量: 完了 iteration 数 (= 別変数を持たない) Snapshot::m_started_time の age 成分 (signed_diff_us_packed で比較)
MyTag(t) t が今登録すべきタグ値 Snapshot::m_started_time (now_us_tagged() 由来の tid 埋め込み µs スタンプ)
TagOlder(a, b) a が古い (= 競合時に勝つ) C++ は started-time µs のみ比較 (tid 除外); spec が tid tie-break で全順序化

iter(t)派生量 である理由

新変数を増やさずに iterBudget[t] から計算できるため、状態空間は 変わらない。 TLC は iter(t) を毎回 MaxCommits - iterBudget[t] と評価するだけで、 新しい状態軸を生まない。
結果: tag 機構を加えても主要な状態爆発は priorityTag(値の組み合わせ)のみ。

なぜ Threads は Nat でなければならないか

cfg からの制約: Threads = {1, 2} のように Nat で定義する必要がある。

状態空間への寄与

変数取りうる値の数
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 部分のみ)

現行 spec では micro cfg 全体での distinct states は 867,696(depth 89)。 (初版 a35c4310 時点は 665,218 — proof_semantics.md §1。同 depth で到達インターリーブが増加)。 priorityTag が掛け算で効くものの、retry 数を有界に保つことで他変数の爆発を抑え、 全体としては 有限 に収まる。

CanProceed(t, n) — CAS の前提条件

LLfree の中核 gate。すべての 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

2 つの disjunct

条件意味C++ 対応
tag = Null 誰も tag を登録していない — 自由参入 Linkage::m_transaction_started_time.load() == 0
tag[2] = t 自分の tag (any iter) — 同一トランザクションの再試行や以前のリトライの遺留 スロットの tid が自分 (Snapshot::m_started_time 上の i_am_privileged_now / fair_mode_blocks_me gate)

旧版にあった ~ActiveThread(tag[2]) (zombie tag 判定) は削除。 タグはコミット成功時にのみ解放されるため、非アクティブ状態は必ず成功経路を経る → stale tag が残ることはなく、このケースは不要。

適用箇所 (priority-gated CAS site)

proof_semantics.md §8: 8/8 priority-gated CAS site で C++ 1:1 対応検証済み。 spec 側の主な call site は以下 10 箇所:

アクションノード意味
BundlePhase1 (superfine prestamp)Parent束ね開始時の serial bump
BundlePhase2Parentparent missing=TRUE で CAS
BundlePhase3 coarsec \in Children全 child を一括 BundledRef 化
BundlePhase3 fine 成功c1 child / step で BundledRef 化
BundlePhase3 fine 失敗c失敗時の rollback + restart
BundlePhase4Parentmissing=FALSE で最終 CAS
CommitParentParentparent payload commit
CommitTryCAStarget[t]child 直接 commit
UnbundleCASAncestorsParentparent missing=TRUE への戻し
UnbundleCASChildtarget[t]child を priority に復帰

典型的な CAS パターン

\* 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 / TagAfterFailpriorityTag を更新する。 これにより「進めない thread は登録だけして待つ」「進めた thread は tag を保持 (Transaction-scope persistence)」という C++ tag_as_contender() 動作を表現する。

「進めない」場合の挙動

CanProceed(t, n)FALSE の場合、そのアクションは enabled でない (TLA+ の意味で「取れない」) → 当該 step では別の thread が動く。 これにより:

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]

4 分岐のラダー

条件動作意図
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 (the per-linkage Linkage::m_transaction_started_time slot
\* is released only on commit success via drop_tags_n_privilege, 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() を呼ばない動作に対応)。

C++ per-linkage priority スロットとの対応

C++TLA+ LLfree
Linkage::m_transaction_started_time 上の i_am_privileged_now / fair_mode_blocks_me gate SnapCheck / CommitParent / CommitTryCAS の前段で CanProceed 判定
CAS 失敗時の tag_as_contender(link) (store-and-verify 書き込み、oldest-wins; m_tagged_linkages に push; spec/C11 が CAS 意味論をモデル化) priorityTag' = [priorityTag EXCEPT ![n] = TagAfterFail(t, n)]
CAS 成功時はストア しない TagAfterSuccess(t, n) == priorityTag[n] (no-op)
commit 成功時に drop_tags_n_privilege()m_tagged_linkages を走査し一致スロットをゼロ化 ClearMyTags(t) at Transaction-end(PreemptTag/ClearMyTags のスライド)

「自分より若い tag を上書きしない」の意味

失敗時に「holder が active で自分が若い」場合、priorityTag は変えない。 これは「古い thread が登録した tag を若い thread が誤って奪う」のを防ぐ。
ただし、若い thread が 先に tag を取った後で 古い thread が来た場合は逆転が必要 → これを担うのが独立アクション PreemptTag (次スライド)。

不変条件: 任意のノード npriorityTag[n] /= Null ならば、 登録された <<iter, tid>> はその瞬間で「最も古い (活性) thread の値」。 zombie tag はコミット成功経路でのみ非アクティブになるため構造的に存在しない。 これが livelock-free 性の局所不変条件 (proof_semantics §4)。

PreemptTag アクション + ClearMyTags

LLfree が追加する独立アクション (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 the symmetric
\*   preempt-window inside C++ tag_as_contender() firing 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 失敗で進めない。
TagAfterFailCAS 試行をした側 しか tag を更新しない (CAS が enabled でないのに Fail にもならないため)。
→ 独立アクション PreemptTagCAS とは別経路で 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++ 対応

C++TLA+ LLfree
tag_as_contender(link) symmetric preempt-window firing 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 性の証明である理由を確認する。

Spec 全体の構造

\* 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 する。

論証チェーン (proof_semantics.md §2)

「終了 + 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 有界 → 状態空間有限 → 上記論証が走る。

cfg 構成 (LLfree)

項目LLfree での扱い
CONSTRAINT なし — 状態空間有限性は priorityTag が保証
SYMMETRY 不使用 (Threads は Nat / liveness 検査と非両立)
Threads {1, 2} (Nat — TagOlder の型整合)
MaxSerial / MaxPayload 未使用
INVARIANT Safety 系 4 つ + TerminalPayloadCheck + QuiescentCheck(micro cfg は加えて DebugSerialBound / PrintTerminalSerial
PROPERTY EventuallyAllDone == <>AllDone
CHECK_DEADLOCK デフォルト TRUE (Terminating disjunct で正規終端を吸収)

検証結果 (現状、2-level LLfree)

cfgThreadsStatesDepthTime結果
micro / fine (MaxCommits=1)2867,69689~35sPass + liveness (laptop)
superfine22,676,1961293:12Pass + liveness
superfine confC (all-root)3137,333,348962h 57minPass + liveness (ohtaka)
MaxCommits=2 superfine2127,586,5993114:40Pass (ohtaka)
dynamic release superfine live2413,884,5163207:13Pass + liveness (ohtaka)

結論: 全構成で safety + livelock-free 性が構造的に証明された。詳細: kamestm/tests/VERIFICATION.md §3doc/verification_log.md

関連仕様

仕様違い解説
BundleUnbundle_3level_LLfree.tlaGrand → Parent → Child 3 段木、再帰的 inner bundle、多段 unbundle walkslides_layer2_LLfree_3level.html
BundleUnbundle_2level_LLfree_dynamic.tla
BundleUnbundle_3level_LLfree_dynamic.tla
オンライン insert(online=true) / release を追加。InsertThreads / ReleaseThreads 等で role 構成可能slides_layer2_LLfree_dynamic.html
BundleUnbundle_hardlink_*.tla (7 仕様)2-parent / 1-child の hard-link topology、Phase 4 reachability gate、bug repro + fix simulation、per-action fairness(条件付き nested_external gate-scope モデル含む)slides_hardlink.html

Privilege=FALSE 検証 + wait-free との区別

priority 機構が必須であることを示す Privilege=FALSE 検証 (proof_semantics §5)、 および「livelock-free」と「wait-free」の混同を防ぐ整理 (proof_semantics §7)。

Privilege=FALSE — priority 機構を無効化した場合

3-level spec (BundleUnbundle_3level_LLfree.tla) では Privilege が 明示的な CONSTANT として存在する。Privilege=FALSE に設定すると priority 機構が すべて無効化される。これは priority 機構が本質的に必要であることを検証するためのスイッチ であり、無効化すると状態空間が発散する。

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 にならない」 ことが 直接 確認できる。

Wait-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 | プロトコル詳細 → slides_layer2.html