A good starting point is to try to think of what properties you want your program to satisfy. This requires either creativity or thinking carefully about what it is you want your program to do. There isn't much to work with here but a plausible property might be "a thread with a given ID will never print a message before a thread with a lower ID". Then you could model your program as follows in Threads.tla:
---------- MODULE Threads ----------
EXTENDS Naturals
VARIABLES activeThreads, hasPrintedMsg
MainThreadId == 1
ThreadID == 1 .. 3
TypeOK ==
/\ activeThreads \subseteq ThreadID
/\ hasPrintedMsg \subseteq ThreadID
PrintCorrectOrder ==
\A tid, otherTid \in ThreadID :
/\ tid \in hasPrintedMsg
/\ otherTid < tid
=> otherTid \in hasPrintedMsg
Init ==
/\ activeThreads = {1}
/\ hasPrintedMsg = {}
Fork(tid) ==
/\ tid \notin activeThreads
/\ activeThreads' = activeThreads \cup {tid}
/\ UNCHANGED hasPrintedMsg
Print(tid) ==
/\ tid \in activeThreads
/\ tid \notin hasPrintedMsg
/\ hasPrintedMsg' = hasPrintedMsg \cup {tid}
/\ UNCHANGED activeThreads
Join(tid) ==
/\ tid /= MainThreadId
/\ tid \in activeThreads
/\ tid \in hasPrintedMsg
/\ activeThreads' = activeThreads \ {tid}
/\ UNCHANGED hasPrintedMsg
Next ==
\E tid \in ThreadID :
\/ Fork(tid)
\/ Print(tid)
\/ Join(tid)
================
You can use this model config file (Threads.cfg) to check the spec with TLC without validating that threads print out in the correct order:
INVARIANTS TypeOK
INIT Init
NEXT Next
TLC should find 32 distinct states with 81 possible state transitions, which seems reasonable. Now we can check whether the threads print their messages in the expected order with this updated model config file:
INVARIANTS TypeOK PrintCorrectOrder
INIT Init
NEXT Next
TLC will output a thread scheduling which violates our print order invariant:
Starting... (2025-11-26 10:10:22)
Computing initial states...
Finished computing initial states: 1 distinct state generated at 2025-11-26 10:10:22.
Error: Invariant PrintCorrectOrder is violated.
Error: The behavior up to this point is:
State 1: <Initial predicate>
/\ hasPrintedMsg = {}
/\ activeThreads = {1}
State 2: <Fork(2) line 25, col 3 to line 27, col 28 of module Threads>
/\ hasPrintedMsg = {}
/\ activeThreads = {1, 2}
State 3: <Print(2) line 30, col 3 to line 33, col 28 of module Threads>
/\ hasPrintedMsg = {2}
/\ activeThreads = {1, 2}
8 states generated, 7 distinct states found, 3 states left on queue.
So that's about all we can do with this spec for safety properties. If you would like an exercise, try to write the safety invariant that the main thread will always outlive the forked threads (although this is more a test of our spec's consistency than a test of the algorithm itself) If you want to experiment with liveness checking there are some additional properties you could write, like "eventually every forked thread joins" or "every thread eventually prints a message", things of this sort.
Andrew