▲ 俯瞰

Layer 2 LL-free (Gen 3)

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

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

このデッキの位置づけ

このあとの流れ: 記法と新出要素 → 検証アプローチの 3 世代 → priorityTagCanProceed → タグ更新規則 → PreemptTag/ClearMyTagsEventuallyAllDone(活性証明)→ 旧世代の意義と wait-free との区別。

記法と新出要素 — base 2level 仕様からの追加分

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 機構は必須」を示す 最も直接的な反証である (末尾スライド「旧世代の保存意義」で詳述)。

新規追加 — 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 または自分のタグ (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 — 全スレッド到達保証

削除 / 緩和されたもの

項目元 2levelLLfree
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 層のみ。

Overview: 検証アプローチの 3 世代

KAME の bundle/unbundle 系 TLA+ 仕様は、有限性をどう確保するかで 3 世代に分かれる。 LLfree は Gen 3 にあたり、本バリアントが CONSTRAINT なしで完全 exhaustion できることが livelock-free 性の構造的証明となる。

3 世代の対比

世代SerialCONSTRAINTPriority 機構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 を構造的に証明

ツリー構造 (元 spec と同じ)

    Parent
      ├── Child1
      └── Child2

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().

TLC verification の結果

665,218 distinct states / depth 89 / 28 秒 / queue 0 (exhaustion)

superfine 構成でも完走

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

priorityTag 変数と補助オペレータ

LLfree が導入する唯一の新変数 priorityTag[n] と、それを操作する補助オペレータ。 これらは全て C++ の m_priority_tidstamp / negotiate() を直接ミラーする。

変数宣言と 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 ==
    /\ ...  \* 元 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++ 対応

オペレータ意味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(値の組み合わせ)のみ。

なぜ 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 部分のみ)

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

2 つの disjunct

条件意味C++ 対応
tag = Null 誰も tag を登録していない — 自由参入 m_priority_tidstamp.load() == 0
tag[2] = t 自分の tag (any iter) — 同一トランザクションの再試行や以前のリトライの遺留 == priority_tidstamp() (自分のスタンプと一致)

旧版にあった ~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++ negotiate() 動作を表現する。

「進めない」場合の挙動

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]

3 段階のラダー

条件動作意図
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() を呼ばない動作に対応)。

C++ 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 のスライド)

「自分より若い 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 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 失敗で進めない。
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
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 性の証明である理由を確認する。

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 の差分 (元 2level vs LLfree)

元 2levelLLfree
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 で正規終端を吸収)

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

cfgThreadsStatesDepthTime結果
coarse micro (MaxCommits=1)2665,2188928sPass + liveness (laptop)
superfine22,676,1961293:12Pass + liveness
superfine confC (all-root)3137,333,348966:35Pass (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 (5 仕様)2-parent / 1-child の hard-link topology、Phase 4 reachability gate、bug repro + fix simulation、per-action fairnessslides_hardlink.html

旧世代の保存意義 + wait-free との区別

LLfree が完成したからといって旧世代仕様を消去すべきではない理由 (proof_semantics §5)、 および「livelock-free」と「wait-free」の混同を防ぐ整理 (proof_semantics §7)。

旧世代を保存している理由 (proof_semantics §5)

Gen 1 (最古, modular serial) — SerialWrapAround violation で落ちる

意義: 「LL-free 機構が必要であること」自体を violation として証明している 貴重な反例。Gen 3 がなぜ必要かの最も説得力ある根拠 (proof_semantics §5)。

Gen 2 (Nat + CONSTRAINT) — 状態数発散

意義: Gen 3 への動機を明示する役割。CONSTRAINT 付きの終了は「打ち切られた領域に 入る前の safety」しか保証せず、liveness は出ない (proof_semantics §3)。

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 性 (構造的)

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

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

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 | 元 spec の解説 → slides_layer2.html