| ------------------------------ MODULE spinbit ------------------------------ |
| EXTENDS TLC, Integers, Sequences, FiniteSets |
| |
| CONSTANT NumThreads |
| CONSTANT NumAcquires |
| CONSTANT NumSpins \* performance tunable "spin" |
| CONSTANT WakeAny \* reduce state space: wake any of the sleepers |
| CONSTANT NULL \* [ model value ] |
| |
| ASSUME NumThreads >= 1 |
| ASSUME NumAcquires >= 0 |
| ASSUME NumSpins >= 0 |
| ASSUME WakeAny \in BOOLEAN |
| |
| Threads == 1..NumThreads |
| |
| (* --algorithm spinbit |
| |
| variables |
| |
| \* logical owner |
| owner = NULL; |
| |
| \* lock state, stored in l.key word |
| l_key_locked = FALSE; \* bit 0 |
| l_key_sleeping = FALSE; \* bit 1 |
| l_key_spinning = FALSE; \* bit 63 |
| |
| \* local variables |
| v_locked = [t \in Threads |-> FALSE]; |
| v_sleeping = [t \in Threads |-> FALSE]; |
| v_spinning = [t \in Threads |-> FALSE]; |
| v8_locked = [t \in Threads |-> FALSE]; |
| v8_sleeping = [t \in Threads |-> FALSE]; |
| weSpin = [t \in Threads |-> FALSE]; |
| i = [t \in Threads |-> 0]; |
| |
| \* OS state |
| sleepers = {}; |
| |
| \* |
| acquisitions = 0; |
| |
| \* performance tunables |
| spin = NumSpins; |
| |
| define |
| |
| TypeInvariant == |
| /\ owner \in Threads \union {NULL} |
| /\ l_key_locked \in BOOLEAN |
| /\ l_key_sleeping \in BOOLEAN |
| /\ l_key_spinning \in BOOLEAN |
| /\ \A t \in Threads: v_locked[t] \in BOOLEAN |
| /\ \A t \in Threads: v_sleeping[t] \in BOOLEAN |
| /\ \A t \in Threads: v_spinning[t] \in BOOLEAN |
| /\ \A t \in Threads: v8_locked[t] \in BOOLEAN |
| /\ \A t \in Threads: v8_sleeping[t] \in BOOLEAN |
| /\ \A t \in Threads: weSpin[t] \in BOOLEAN |
| /\ \A t \in Threads: i[t] \in Nat |
| /\ sleepers \subseteq Threads |
| /\ acquisitions \in Nat |
| |
| NoLostWakeups == |
| (Cardinality(sleepers) > 0) ~> (Cardinality(sleepers) = 0) |
| |
| HaveAcquisitions == |
| <>[](acquisitions >= NumAcquires) |
| |
| end define; |
| |
| \* This is a lock, so if a process dies while holding it the rest of the simulation |
| \* won't be able to make progress. We need "fair process" so the threads don't die. |
| fair process thread \in Threads |
| begin |
| NonCriticalSection: |
| if acquisitions >= NumAcquires then |
| goto Done; |
| end if; |
| |
| SpeculativeGrab: |
| \* v8 = atomic.Xchg8(k8, mutexLocked) |
| v8_locked[self] := l_key_locked; |
| v8_sleeping[self] := l_key_sleeping; |
| l_key_locked := TRUE; |
| l_key_sleeping := FALSE; |
| SpeculativeGrabCheck: |
| if v8_locked[self] = FALSE then |
| if v8_sleeping[self] = TRUE then |
| \* atomic.Or8(k8, mutexSleeping) |
| l_key_sleeping := TRUE; |
| end if; |
| goto Locked; |
| else |
| \* var v uintptr = mutexLocked |
| v_locked[self] := TRUE; |
| v_sleeping[self] := FALSE; |
| v_spinning[self] := FALSE; |
| weSpin[self] := FALSE; |
| end if; |
| |
| EnterSlowPath: |
| i[self] := 0; |
| |
| TryAcquire: |
| if v_locked[self] = TRUE then |
| goto SetSleepBit |
| else |
| if weSpin[self] then |
| \* next := (v &^ mutexSpinning) | mutexLocked | mutexSleeping |
| \* if atomic.Casuintptr(&l.key, v, next) { |
| if v_locked[self] = l_key_locked |
| /\ v_sleeping[self] = l_key_sleeping |
| /\ v_spinning[self] = l_key_spinning then |
| l_key_locked := TRUE; |
| l_key_sleeping := TRUE; |
| l_key_spinning := FALSE; |
| goto Locked; |
| end if; |
| else |
| \* prev8 := atomic.Xchg8(k8, mutexLocked|mutexSleeping) |
| \* if prev8&mutexLocked == 0 { |
| if l_key_locked = FALSE then |
| l_key_locked := TRUE; |
| l_key_sleeping := TRUE; |
| goto Locked; |
| else |
| l_key_locked := TRUE; |
| l_key_sleeping := TRUE; |
| end if; |
| end if; |
| end if; |
| TryAcquireLoop: |
| \* v = atomic.Loaduintptr(&l.key) |
| v_locked[self] := l_key_locked; |
| v_sleeping[self] := l_key_sleeping; |
| v_spinning[self] := l_key_spinning; |
| goto TryAcquire; |
| |
| SetSleepBit: |
| \* atomic.Or8(k8, mutexSleeping) |
| if v_sleeping[self] = FALSE then |
| l_key_sleeping := TRUE; |
| v_sleeping[self] := TRUE; |
| end if; |
| |
| SetSpinBit: |
| \* if !weSpin && atomic.Xchg8(key8Upper(&l.key), mutexSpinning>>((goarch.PtrSize-1)*8)) == 0 { |
| if weSpin[self] = FALSE then |
| weSpin[self] := (l_key_spinning = FALSE); |
| l_key_spinning := TRUE; |
| end if; |
| |
| DoSpin: |
| if weSpin[self] = TRUE then |
| if i[self] < spin then |
| \* procyield(active_spin_cnt) |
| \* v = atomic.Loaduintptr(&l.key) |
| v_locked[self] := l_key_locked; |
| v_sleeping[self] := l_key_sleeping; |
| v_spinning[self] := l_key_spinning; |
| goto TryAcquire; |
| else |
| weSpin[self] := FALSE; |
| \* atomic.Xchg8(key8Upper(&l.key), 0) |
| l_key_spinning := FALSE; |
| end if; |
| end if; |
| |
| Sleep: |
| \* v = atomic.Loaduintptr(&l.key) |
| v_locked[self] := l_key_locked; |
| v_sleeping[self] := l_key_sleeping; |
| v_spinning[self] := l_key_spinning; |
| Futex: |
| if v_locked[self] = FALSE then |
| goto TryAcquire; |
| else |
| \* futexsleep(k32, uint32(v|mutexSleeping), -1) |
| if l_key_locked = v_locked[self] |
| /\ l_key_sleeping = TRUE then |
| sleepers := sleepers \union {self}; |
| end if; |
| end if; |
| |
| Sleeping: |
| await self \notin sleepers; |
| i[self] := 0; |
| \* v = atomic.Loaduintptr(&l.key) |
| v_locked[self] := l_key_locked; |
| v_sleeping[self] := l_key_sleeping; |
| v_spinning[self] := l_key_spinning; |
| goto TryAcquire; |
| |
| Locked: |
| owner := self; |
| CriticalSection: |
| acquisitions := acquisitions + 1; |
| Unlock: |
| owner := NULL; |
| \* prev8 := atomic.Xchg8(key8(&l.key), 0) |
| v8_locked[self] := l_key_locked; |
| v8_sleeping[self] := l_key_sleeping; |
| l_key_locked := FALSE; |
| l_key_sleeping := FALSE; |
| if v8_sleeping[self] = TRUE then |
| goto Wakeup; |
| else |
| goto NonCriticalSection; |
| end if; |
| |
| Wakeup: |
| \* v := atomic.Loaduintptr(&l.key) |
| v_locked[self] := l_key_locked; |
| v_sleeping[self] := l_key_sleeping; |
| v_spinning[self] := l_key_spinning; |
| Wake2: |
| if v_spinning[self] = FALSE then |
| if Cardinality(sleepers) > 0 then |
| if WakeAny then |
| with other = CHOOSE other \in sleepers: TRUE do |
| sleepers := sleepers \ {other}; |
| end with; |
| else |
| with other \in sleepers do |
| sleepers := sleepers \ {other}; |
| end with; |
| end if; |
| end if; |
| end if; |
| goto NonCriticalSection; |
| |
| end process; |
| |
| end algorithm; *) |
| \* BEGIN TRANSLATION (chksum(pcal) = "a993096" /\ chksum(tla) = "41bbe278") |
| VARIABLES owner, l_key_locked, l_key_sleeping, l_key_spinning, v_locked, |
| v_sleeping, v_spinning, v8_locked, v8_sleeping, weSpin, i, sleepers, |
| acquisitions, spin, pc |
| |
| (* define statement *) |
| TypeInvariant == |
| /\ owner \in Threads \union {NULL} |
| /\ l_key_locked \in BOOLEAN |
| /\ l_key_sleeping \in BOOLEAN |
| /\ l_key_spinning \in BOOLEAN |
| /\ \A t \in Threads: v_locked[t] \in BOOLEAN |
| /\ \A t \in Threads: v_sleeping[t] \in BOOLEAN |
| /\ \A t \in Threads: v_spinning[t] \in BOOLEAN |
| /\ \A t \in Threads: v8_locked[t] \in BOOLEAN |
| /\ \A t \in Threads: v8_sleeping[t] \in BOOLEAN |
| /\ \A t \in Threads: weSpin[t] \in BOOLEAN |
| /\ \A t \in Threads: i[t] \in Nat |
| /\ sleepers \subseteq Threads |
| /\ acquisitions \in Nat |
| |
| NoLostWakeups == |
| (Cardinality(sleepers) > 0) ~> (Cardinality(sleepers) = 0) |
| |
| HaveAcquisitions == |
| <>[](acquisitions >= NumAcquires) |
| |
| |
| vars == << owner, l_key_locked, l_key_sleeping, l_key_spinning, v_locked, |
| v_sleeping, v_spinning, v8_locked, v8_sleeping, weSpin, i, |
| sleepers, acquisitions, spin, pc >> |
| |
| ProcSet == (Threads) |
| |
| Init == (* Global variables *) |
| /\ owner = NULL |
| /\ l_key_locked = FALSE |
| /\ l_key_sleeping = FALSE |
| /\ l_key_spinning = FALSE |
| /\ v_locked = [t \in Threads |-> FALSE] |
| /\ v_sleeping = [t \in Threads |-> FALSE] |
| /\ v_spinning = [t \in Threads |-> FALSE] |
| /\ v8_locked = [t \in Threads |-> FALSE] |
| /\ v8_sleeping = [t \in Threads |-> FALSE] |
| /\ weSpin = [t \in Threads |-> FALSE] |
| /\ i = [t \in Threads |-> 0] |
| /\ sleepers = {} |
| /\ acquisitions = 0 |
| /\ spin = NumSpins |
| /\ pc = [self \in ProcSet |-> "NonCriticalSection"] |
| |
| NonCriticalSection(self) == /\ pc[self] = "NonCriticalSection" |
| /\ IF acquisitions >= NumAcquires |
| THEN /\ pc' = [pc EXCEPT ![self] = "Done"] |
| ELSE /\ pc' = [pc EXCEPT ![self] = "SpeculativeGrab"] |
| /\ UNCHANGED << owner, l_key_locked, |
| l_key_sleeping, l_key_spinning, |
| v_locked, v_sleeping, v_spinning, |
| v8_locked, v8_sleeping, weSpin, i, |
| sleepers, acquisitions, spin >> |
| |
| SpeculativeGrab(self) == /\ pc[self] = "SpeculativeGrab" |
| /\ v8_locked' = [v8_locked EXCEPT ![self] = l_key_locked] |
| /\ v8_sleeping' = [v8_sleeping EXCEPT ![self] = l_key_sleeping] |
| /\ l_key_locked' = TRUE |
| /\ l_key_sleeping' = FALSE |
| /\ pc' = [pc EXCEPT ![self] = "SpeculativeGrabCheck"] |
| /\ UNCHANGED << owner, l_key_spinning, v_locked, |
| v_sleeping, v_spinning, weSpin, i, |
| sleepers, acquisitions, spin >> |
| |
| SpeculativeGrabCheck(self) == /\ pc[self] = "SpeculativeGrabCheck" |
| /\ IF v8_locked[self] = FALSE |
| THEN /\ IF v8_sleeping[self] = TRUE |
| THEN /\ l_key_sleeping' = TRUE |
| ELSE /\ TRUE |
| /\ UNCHANGED l_key_sleeping |
| /\ pc' = [pc EXCEPT ![self] = "Locked"] |
| /\ UNCHANGED << v_locked, v_sleeping, |
| v_spinning, weSpin >> |
| ELSE /\ v_locked' = [v_locked EXCEPT ![self] = TRUE] |
| /\ v_sleeping' = [v_sleeping EXCEPT ![self] = FALSE] |
| /\ v_spinning' = [v_spinning EXCEPT ![self] = FALSE] |
| /\ weSpin' = [weSpin EXCEPT ![self] = FALSE] |
| /\ pc' = [pc EXCEPT ![self] = "EnterSlowPath"] |
| /\ UNCHANGED l_key_sleeping |
| /\ UNCHANGED << owner, l_key_locked, |
| l_key_spinning, v8_locked, |
| v8_sleeping, i, sleepers, |
| acquisitions, spin >> |
| |
| EnterSlowPath(self) == /\ pc[self] = "EnterSlowPath" |
| /\ i' = [i EXCEPT ![self] = 0] |
| /\ pc' = [pc EXCEPT ![self] = "TryAcquire"] |
| /\ UNCHANGED << owner, l_key_locked, l_key_sleeping, |
| l_key_spinning, v_locked, v_sleeping, |
| v_spinning, v8_locked, v8_sleeping, |
| weSpin, sleepers, acquisitions, spin >> |
| |
| TryAcquire(self) == /\ pc[self] = "TryAcquire" |
| /\ IF v_locked[self] = TRUE |
| THEN /\ pc' = [pc EXCEPT ![self] = "SetSleepBit"] |
| /\ UNCHANGED << l_key_locked, l_key_sleeping, |
| l_key_spinning >> |
| ELSE /\ IF weSpin[self] |
| THEN /\ IF v_locked[self] = l_key_locked |
| /\ v_sleeping[self] = l_key_sleeping |
| /\ v_spinning[self] = l_key_spinning |
| THEN /\ l_key_locked' = TRUE |
| /\ l_key_sleeping' = TRUE |
| /\ l_key_spinning' = FALSE |
| /\ pc' = [pc EXCEPT ![self] = "Locked"] |
| ELSE /\ pc' = [pc EXCEPT ![self] = "TryAcquireLoop"] |
| /\ UNCHANGED << l_key_locked, |
| l_key_sleeping, |
| l_key_spinning >> |
| ELSE /\ IF l_key_locked = FALSE |
| THEN /\ l_key_locked' = TRUE |
| /\ l_key_sleeping' = TRUE |
| /\ pc' = [pc EXCEPT ![self] = "Locked"] |
| ELSE /\ l_key_locked' = TRUE |
| /\ l_key_sleeping' = TRUE |
| /\ pc' = [pc EXCEPT ![self] = "TryAcquireLoop"] |
| /\ UNCHANGED l_key_spinning |
| /\ UNCHANGED << owner, v_locked, v_sleeping, v_spinning, |
| v8_locked, v8_sleeping, weSpin, i, |
| sleepers, acquisitions, spin >> |
| |
| TryAcquireLoop(self) == /\ pc[self] = "TryAcquireLoop" |
| /\ v_locked' = [v_locked EXCEPT ![self] = l_key_locked] |
| /\ v_sleeping' = [v_sleeping EXCEPT ![self] = l_key_sleeping] |
| /\ v_spinning' = [v_spinning EXCEPT ![self] = l_key_spinning] |
| /\ pc' = [pc EXCEPT ![self] = "TryAcquire"] |
| /\ UNCHANGED << owner, l_key_locked, l_key_sleeping, |
| l_key_spinning, v8_locked, v8_sleeping, |
| weSpin, i, sleepers, acquisitions, |
| spin >> |
| |
| SetSleepBit(self) == /\ pc[self] = "SetSleepBit" |
| /\ IF v_sleeping[self] = FALSE |
| THEN /\ l_key_sleeping' = TRUE |
| /\ v_sleeping' = [v_sleeping EXCEPT ![self] = TRUE] |
| ELSE /\ TRUE |
| /\ UNCHANGED << l_key_sleeping, v_sleeping >> |
| /\ pc' = [pc EXCEPT ![self] = "SetSpinBit"] |
| /\ UNCHANGED << owner, l_key_locked, l_key_spinning, |
| v_locked, v_spinning, v8_locked, |
| v8_sleeping, weSpin, i, sleepers, |
| acquisitions, spin >> |
| |
| SetSpinBit(self) == /\ pc[self] = "SetSpinBit" |
| /\ IF weSpin[self] = FALSE |
| THEN /\ weSpin' = [weSpin EXCEPT ![self] = (l_key_spinning = FALSE)] |
| /\ l_key_spinning' = TRUE |
| ELSE /\ TRUE |
| /\ UNCHANGED << l_key_spinning, weSpin >> |
| /\ pc' = [pc EXCEPT ![self] = "DoSpin"] |
| /\ UNCHANGED << owner, l_key_locked, l_key_sleeping, |
| v_locked, v_sleeping, v_spinning, |
| v8_locked, v8_sleeping, i, sleepers, |
| acquisitions, spin >> |
| |
| DoSpin(self) == /\ pc[self] = "DoSpin" |
| /\ IF weSpin[self] = TRUE |
| THEN /\ IF i[self] < spin |
| THEN /\ v_locked' = [v_locked EXCEPT ![self] = l_key_locked] |
| /\ v_sleeping' = [v_sleeping EXCEPT ![self] = l_key_sleeping] |
| /\ v_spinning' = [v_spinning EXCEPT ![self] = l_key_spinning] |
| /\ pc' = [pc EXCEPT ![self] = "TryAcquire"] |
| /\ UNCHANGED << l_key_spinning, weSpin >> |
| ELSE /\ weSpin' = [weSpin EXCEPT ![self] = FALSE] |
| /\ l_key_spinning' = FALSE |
| /\ pc' = [pc EXCEPT ![self] = "Sleep"] |
| /\ UNCHANGED << v_locked, v_sleeping, |
| v_spinning >> |
| ELSE /\ pc' = [pc EXCEPT ![self] = "Sleep"] |
| /\ UNCHANGED << l_key_spinning, v_locked, |
| v_sleeping, v_spinning, weSpin >> |
| /\ UNCHANGED << owner, l_key_locked, l_key_sleeping, v8_locked, |
| v8_sleeping, i, sleepers, acquisitions, spin >> |
| |
| Sleep(self) == /\ pc[self] = "Sleep" |
| /\ v_locked' = [v_locked EXCEPT ![self] = l_key_locked] |
| /\ v_sleeping' = [v_sleeping EXCEPT ![self] = l_key_sleeping] |
| /\ v_spinning' = [v_spinning EXCEPT ![self] = l_key_spinning] |
| /\ pc' = [pc EXCEPT ![self] = "Futex"] |
| /\ UNCHANGED << owner, l_key_locked, l_key_sleeping, |
| l_key_spinning, v8_locked, v8_sleeping, weSpin, |
| i, sleepers, acquisitions, spin >> |
| |
| Futex(self) == /\ pc[self] = "Futex" |
| /\ IF v_locked[self] = FALSE |
| THEN /\ pc' = [pc EXCEPT ![self] = "TryAcquire"] |
| /\ UNCHANGED sleepers |
| ELSE /\ IF l_key_locked = v_locked[self] |
| /\ l_key_sleeping = TRUE |
| THEN /\ sleepers' = (sleepers \union {self}) |
| ELSE /\ TRUE |
| /\ UNCHANGED sleepers |
| /\ pc' = [pc EXCEPT ![self] = "Sleeping"] |
| /\ UNCHANGED << owner, l_key_locked, l_key_sleeping, |
| l_key_spinning, v_locked, v_sleeping, |
| v_spinning, v8_locked, v8_sleeping, weSpin, i, |
| acquisitions, spin >> |
| |
| Sleeping(self) == /\ pc[self] = "Sleeping" |
| /\ self \notin sleepers |
| /\ i' = [i EXCEPT ![self] = 0] |
| /\ v_locked' = [v_locked EXCEPT ![self] = l_key_locked] |
| /\ v_sleeping' = [v_sleeping EXCEPT ![self] = l_key_sleeping] |
| /\ v_spinning' = [v_spinning EXCEPT ![self] = l_key_spinning] |
| /\ pc' = [pc EXCEPT ![self] = "TryAcquire"] |
| /\ UNCHANGED << owner, l_key_locked, l_key_sleeping, |
| l_key_spinning, v8_locked, v8_sleeping, |
| weSpin, sleepers, acquisitions, spin >> |
| |
| Locked(self) == /\ pc[self] = "Locked" |
| /\ owner' = self |
| /\ pc' = [pc EXCEPT ![self] = "CriticalSection"] |
| /\ UNCHANGED << l_key_locked, l_key_sleeping, l_key_spinning, |
| v_locked, v_sleeping, v_spinning, v8_locked, |
| v8_sleeping, weSpin, i, sleepers, acquisitions, |
| spin >> |
| |
| CriticalSection(self) == /\ pc[self] = "CriticalSection" |
| /\ acquisitions' = acquisitions + 1 |
| /\ pc' = [pc EXCEPT ![self] = "Unlock"] |
| /\ UNCHANGED << owner, l_key_locked, l_key_sleeping, |
| l_key_spinning, v_locked, v_sleeping, |
| v_spinning, v8_locked, v8_sleeping, |
| weSpin, i, sleepers, spin >> |
| |
| Unlock(self) == /\ pc[self] = "Unlock" |
| /\ owner' = NULL |
| /\ v8_locked' = [v8_locked EXCEPT ![self] = l_key_locked] |
| /\ v8_sleeping' = [v8_sleeping EXCEPT ![self] = l_key_sleeping] |
| /\ l_key_locked' = FALSE |
| /\ l_key_sleeping' = FALSE |
| /\ IF v8_sleeping'[self] = TRUE |
| THEN /\ pc' = [pc EXCEPT ![self] = "Wakeup"] |
| ELSE /\ pc' = [pc EXCEPT ![self] = "NonCriticalSection"] |
| /\ UNCHANGED << l_key_spinning, v_locked, v_sleeping, |
| v_spinning, weSpin, i, sleepers, acquisitions, |
| spin >> |
| |
| Wakeup(self) == /\ pc[self] = "Wakeup" |
| /\ v_locked' = [v_locked EXCEPT ![self] = l_key_locked] |
| /\ v_sleeping' = [v_sleeping EXCEPT ![self] = l_key_sleeping] |
| /\ v_spinning' = [v_spinning EXCEPT ![self] = l_key_spinning] |
| /\ pc' = [pc EXCEPT ![self] = "Wake2"] |
| /\ UNCHANGED << owner, l_key_locked, l_key_sleeping, |
| l_key_spinning, v8_locked, v8_sleeping, weSpin, |
| i, sleepers, acquisitions, spin >> |
| |
| Wake2(self) == /\ pc[self] = "Wake2" |
| /\ IF v_spinning[self] = FALSE |
| THEN /\ IF Cardinality(sleepers) > 0 |
| THEN /\ IF WakeAny |
| THEN /\ LET other == CHOOSE other \in sleepers: TRUE IN |
| sleepers' = sleepers \ {other} |
| ELSE /\ \E other \in sleepers: |
| sleepers' = sleepers \ {other} |
| ELSE /\ TRUE |
| /\ UNCHANGED sleepers |
| ELSE /\ TRUE |
| /\ UNCHANGED sleepers |
| /\ pc' = [pc EXCEPT ![self] = "NonCriticalSection"] |
| /\ UNCHANGED << owner, l_key_locked, l_key_sleeping, |
| l_key_spinning, v_locked, v_sleeping, |
| v_spinning, v8_locked, v8_sleeping, weSpin, i, |
| acquisitions, spin >> |
| |
| thread(self) == NonCriticalSection(self) \/ SpeculativeGrab(self) |
| \/ SpeculativeGrabCheck(self) \/ EnterSlowPath(self) |
| \/ TryAcquire(self) \/ TryAcquireLoop(self) |
| \/ SetSleepBit(self) \/ SetSpinBit(self) \/ DoSpin(self) |
| \/ Sleep(self) \/ Futex(self) \/ Sleeping(self) |
| \/ Locked(self) \/ CriticalSection(self) \/ Unlock(self) |
| \/ Wakeup(self) \/ Wake2(self) |
| |
| (* Allow infinite stuttering to prevent deadlock on termination. *) |
| Terminating == /\ \A self \in ProcSet: pc[self] = "Done" |
| /\ UNCHANGED vars |
| |
| Next == (\E self \in Threads: thread(self)) |
| \/ Terminating |
| |
| Spec == /\ Init /\ [][Next]_vars |
| /\ \A self \in Threads : WF_vars(thread(self)) |
| |
| Termination == <>(\A self \in ProcSet: pc[self] = "Done") |
| |
| \* END TRANSLATION |
| |
| ============================================================================= |
| \* Modification History |
| \* Last modified Tue Oct 01 13:18:42 PDT 2024 by rhysh |
| \* Created Tue Sep 17 12:10:43 PDT 2024 by rhysh |