-
Notifications
You must be signed in to change notification settings - Fork 24
Parallelization and Threading
- Multithreading Constructs
- Object Isolation:
snapshotvstransaction - Thread API
- Threads vs Engines
- What is a Template?
- Engine Options Format
- Engine API
MeTTa supports execution blocks with strict isolation guarantees using snapshot and transaction. These control whether the system commits changes or rolls them back after a logic block executes.
| Construct | Commits Changes? | Reentrant? | Rollback on Failure? | Use Case |
|---|---|---|---|---|
snapshot |
❌ (never) | ✅ | ✅ | Read-only or speculative logic |
transaction |
✅ (on success) | ❌ | ✅ | Batched or atomic side effects |
These are exposed via the thread system (from !(import &self threads)):
| Function | Purpose |
|---|---|
thread:snapshot! |
Run block, discard changes |
thread:transaction! |
Run block, commit changes on success |
thread:current-transaction! |
Return current transaction context |
thread:transaction-updates! |
List pending side effects |
thread:show-transaction! |
Pretty-print pending commit actions |
(snapshot
(= (temp-fn $x) (* $x 2))
(temp-fn 4))
temp-fnis not retained globally — it exists only within this block.
Use Cases:
- Simulating "what if?" logic
- Optimistic reads
- Avoiding accidental mutations
- Query planning and dry-runs
(transaction
(= (shared-fn $x) (+ $x 1)))
shared-fnis only defined globally if no failures occur in the block.
Use Cases:
- Multi-step updates
- Controlled side effects (
add-atom,remove-atom) - Safe graph mutation or knowledge base editing
(transaction
(remove-atom (balance Alice $b))
(add-atom (balance Alice (- $b 100)))
(print "Debited $100 from Alice"))If any subgoal fails, no changes are made. This transactional pattern is useful for atomic state updates.
-
Use
snapshotwhen:- You need speculative logic
- You want a read-only version of the world
- You are evaluating possibilities without consequences
-
Use
transactionwhen:- Multiple changes must be committed atomically
- You want to guarantee rollback on error
- You're modifying a shared world model
-
Avoid I/O inside
transactionunless the runtime supports side-effect buffering or compensation.
!(snapshot
(= (score-preview $name)
(sequential
(add-atom &scores (score $name 42))
(match &scores (score $name $s) $s))))
!(transaction
(sequential
(remove-atom &self (status Bob waiting))
(add-atom &self (status Bob active))))This pattern ensures correct reasoning and mutation separation, making MeTTa safe for concurrent and logic-dependent workflows.
| Function | Description |
|---|---|
thread:spawn! |
Create thread with expression |
thread:spawn-lazy! |
Lazy thread eval |
thread:async! |
Async spawn, detached |
thread:await!, await-token!
|
Wait for result |
thread:timeout! |
Timeout wrapper |
thread:completed? |
Check if finished |
thread:limit-results! |
Cap result count from thread |
| Function | Description |
|---|---|
thread:hyperpose! |
Run all and collect results |
thread:race! |
Return first completed result |
thread:limit-time! |
Impose deadline |
| Function | Description |
|---|---|
thread:mutex-create! |
Make a new lock |
thread:mutex-lock! |
Acquire mutex |
thread:mutex-unlock! |
Release mutex |
thread:mutex-with! |
Scoped lock context |
| Function | Description |
|---|---|
thread:queue-create! |
Create a message queue |
thread:send-message! |
Send message to a queue |
thread:receive-message! |
Receive from queue |
| Function | Description |
|---|---|
thread:join! |
Wait for thread completion |
thread:detach! |
Let thread run independently |
thread:suspend!, resume!
|
Pause and resume |
thread:cancel! |
Force thread termination |
thread:status! |
Query thread status |
thread:self! |
Get current thread ID |
thread:list! |
List all running threads |
thread:sleep! |
Sleep for seconds |
thread:set-priority! |
Adjust thread priority |
!(thread:error! $thread "Failed to join")(thread:spawn! (my-computation))- Executes expression in its own engine
- Tied to thread lifecycle
!(let $e (engine:create (between-fn 1 3))
(engine:next $e)) -> 1,2,3- Can be stepped
- Shared across threads
| Use Case | Thread API | Engine API |
|---|---|---|
| Fire-and-forget tasks | ✅ | ❌ |
| Lazy streams | ❌ | ✅ |
| Share across threads | ⚠ risky | ✅ |
| Debuggable/step control | ❌ | ✅ |
| Construct | Parallel? | Shared Vars? | Notes |
|---|---|---|---|
progn |
Maybe | Maybe | Chooses between sequential-i/s or parallel-i/s based on dependency |
sequential |
No | Maybe | Ordered execution, runtime analyzed |
parallel |
Yes | Maybe | Parallel tasks, shared if safe |
sequential-s |
No | Yes | Sequential with shared variables |
parallel-s |
Yes | Yes | Parallel with shared variables |
sequential-i |
No | No | Explicit full isolation |
parallel-i |
Yes | No | Fully sandboxed parallel execution |
isolated |
Maybe | No | Chooses between sequential-i or parallel-i based on analysis |
shared |
Maybe | Yes | Chooses between sequential-s or parallel-s based on dependency |
shared and isolated automatically select safe, optimal execution strategies via runtime analysis:
-
shareddispatches tosequential-sorparallel-sdepending on data dependencies -
isolateddispatches tosequential-iorparallel-iwhen no dependency is detected
Great — I’ll now rewrite the full Execution Examples section with that level of detail for each case. These explanations will describe:
-
Evaluation strategy (
sequential,parallel, etc.) - Variable sharing and flow
- Implications for correctness and performance
- Typical use cases
(sequential
(isolated (Task1 $x) (Task2 $y) (Task3 $x $y))
(shared (Task4 $x) (Task5 $y) (Task6 $x $y)))Behavior:
-
sequentialenforces strict top-to-bottom execution. -
The first group is
isolated, so all threeTask1,Task2, andTask3run in a sandbox — they do not see global bindings. -
However, they share values between each other:
-
Task1binds$x -
Task2binds$y -
Task3uses both
-
-
The second group is
shared, meaning$xand$ymust be available globally or from outside the block.
Use case: Complex state mutation followed by interpretation or reporting using shared values.
(parallel
(isolated (Task1 $x) (Task2 $y) (Task3 $x $y))
(shared (Task4 $x) (Task5 $y) (Task6 $x $y)))Behavior:
- All top-level sub-blocks (
isolated,shared) run in parallel. - Within the
isolatedgroup, tasks execute sequentially as before, but isolated. - The
sharedblock runs in parallel among its tasks.
Effect:
- Increases throughput if tasks are independent.
- Requires care:
Task4–Task6may begin while$x/$yare still being resolved elsewhere unless managed carefully.
Use case: Data preparation (in isolated) concurrent with visualization or export (in shared).
(progn
(isolated (Task1 $x) (Task2 $y) (Task3 $x $y))
(shared (Task4 $x) (Task5 $y) (Task6 $x $y)))Behavior:
-
prognis likesequential, but softer semantically — often used to enforce readable ordering. - All expressions are guaranteed to run in order.
- Unlike
parallel, nothing runs concurrently.
Use case: When both isolation and data reuse are needed, and correctness matters more than speed (e.g., in audit logging or interactive sessions).
(shared
(parallel (Task1 $x) (Task2 $y) (Task3 $x $y))
(sequential (Task4 $x) (Task5 $y) (Task6 $x $y)))Behavior:
-
Outer
sharedmeans all sub-blocks can reuse and unify variables. -
Inside, the
parallelgroup runs all three tasks at once.-
$x,$ymay be bound independently, assuming safe parallel access.
-
-
The
sequentialblock executes in order and can rely on previous bindings.
Use case: Running independent inferences in parallel, then serially applying them in a post-processing pipeline.
(isolated
(parallel (Task1 $x) (Task2 $y) (Task3 $x $y))
(sequential (Task4 $x) (Task5 $y) (Task6 $x $y)))Behavior:
- All sub-blocks execute inside a sandbox.
-
parallelgroup runs first, computing$xand$yin isolation. - Then
sequentialblock consumes those isolated bindings in strict order.
Effect:
- No risk of corrupting global state.
- Result is a contained environment where
Task1–Task6are run and discarded unless committed explicitly.
Use case: Transactional logic evaluation, such as test runs or inference previews.
(isolated
(parallel (Task1 $x) (Task2 $y) (Task3 $x $y))
(Task4 $x)
(Task5 $y)
(Task6 $x $y))Behavior:
- Everything is inside a single
isolatedblock. -
Task1–Task3are computed concurrently. -
Task4–Task6execute sequentially afterward (implicitly ordered).
Binding Flow:
-
$xand$yfromTask1andTask2are available insideTask4–Task6, but all results are discarded after theisolatedblock finishes.
Use case: Large speculative computation inside a non-committing scope.
(shared
(parallel-i (Task1 $x) (Task2 $y) (Task3 $x $y))
(Task4 $x)
(Task5 $y)
(Task6 $x $y))Behavior:
-
Outer
sharedallows global binding visibility. -
The
parallel-igroup runs 3 fully isolated tasks concurrently:-
Task1binds$x -
Task2binds$y -
Task3may use both, but in its own sandbox
-
-
After those tasks complete, the results (if successful) are unified into shared space.
Postprocessing:
-
Task4,Task5, andTask6run afterward with shared access to$xand$y.
Use case: Isolated inference generation followed by shared knowledge integration.
MeTTaLog’s multithreading model provides a hybrid of automatic inference and declarative control. It introduces shared and isolated forms that serve as smart defaults, inferring optimal execution strategy based on data dependencies and runtime conditions.
These wrappers allow beginners and intermediate users to write concurrent logic without over-specifying control flow. For example:
-
sharedwraps expressions that reuse or unify variables (e.g.$x,$y) and may need synchronization -
isolatedassumes no shared state and permits safe, sandboxed execution
When needed, explicit annotations are available:
- Use
sequentialwhen execution order matters (e.g., logging, I/O,add-atom,remove-atom) - Use
parallelwhen you have manually verified independence or commutative effects
This model is designed to:
- Let experts control evaluation precisely
- Let intermediate users write flexible, parallel-safe code
- Let beginners explore without breakage or nondeterministic failures
Why sequential and parallel still exist:
Static inference cannot always detect dangerous global effects, such as:
- Modifications to shared state (
add-atom,remove-atom) - External I/O (user interfaces, logs)
In these cases, using sequential is mandatory to ensure correctness. parallel signals a confident assertion that side effects are benign or isolated.
Best practices:
- Default to
sharedorisolatedunless you're sure - Use
sequentialandparallelto enforce semantic correctness or optimize for performance - Think of
sharedlike Lisp'sprogn: a neutral ordered container that defers concurrency decisions
This declarative strategy lets the runtime and programmer cooperate in choosing the safest and fastest execution model.