Synchronization and Atomic Plans
An agent can have several intentions in progress. Their statements can interleave, so a sequence that reads a belief and then updates it can use stale state. ASTRA offers synchronized plans and blocks for mutual exclusion, and atomic plans for controlling intention scheduling.
These mechanisms operate within one agent. They do not lock another agent’s belief base or a shared external resource such as a CArtAgO artifact.
Synchronized blocks
A synchronized block obtains a named lock before executing its statements:
synchronized(counter) {
query(total(int N));
-total(N);
+total(N + 1);
}
Here counter is a literal lock name, not a variable whose value is evaluated. Other intentions entering a block with the same name wait until its owner releases it. Blocks using different names do not exclude each other. Plans that do not use the lock can still run and modify the same beliefs, so every competing access must follow the same locking convention.
Put the read inside the block. Reading total(N) in a plan’s context binds N before the lock is acquired, so it can be out of date by the time the body runs.
Synchronized plans
To protect a whole plan body, use synchronized<name> plan:
synchronized<counter> plan +!increment() {
query(total(int N));
-total(N);
+total(N + 1);
}
This is equivalent to wrapping the body in synchronized(counter) { ... }. The plan context is still evaluated before locking. The unnamed form, synchronized plan, uses the default lock named synchronized; all unnamed synchronized plans in the agent share it.
Example: competing increments
Save this as src/main/astra/Main.astra in a project created using the Quick Start:
agent Main {
module Console console;
module System system;
types counters {
formula total(int);
formula completed(string);
}
initial total(0);
plan +!main(list args) {
!!increment("first");
!!increment("second");
wait(completed("first") & completed("second"));
query(total(int N));
console.println("total = " + N);
system.exit();
}
synchronized<counter> plan +!increment(string Worker) {
query(total(int N));
-total(N);
+total(N + 1);
+completed(Worker);
}
}
Run mvn clean compile astra:run. The two !! calls create separate intentions. Each obtains the same lock before reading and changing the counter. After both finish, the agent prints total = 2 with its agent-name prefix.
Keep critical sections short
A synchronized block retains its lock while its intention is suspended. Avoid waiting for a goal, message or condition whose completion requires another intention to obtain that same lock. Likewise, if several locks are needed, acquire them in a consistent order to avoid cycles of waiting.
On normal completion the lock is released. The synchronized statement also releases its acquired lock when failure unwinds through it. A surrounding try/recover can handle the failure, but changes already made to beliefs or external state are not rolled back.
Atomic plans
Declare an atomic plan with atomic plan:
atomic plan +!update() {
query(total(int N));
-total(N);
+total(N + 1);
}
An intention created from an atomic plan stays at the front of the agent’s intention queue when selected, rather than rotating behind other intentions. This keeps other intentions from taking execution steps until it completes. Keep atomic work short and finite.
Atomic does not mean transactional. Belief changes and external effects are not undone on failure. Nor does it stop the interpreter’s perception, belief-update or event-handling phases: those still run between execution steps. Other agents continue to run independently.
Suspension and subgoals
In ASTRA 2.3.0, a selected atomic intention continues to occupy the front of the queue when suspended. The scheduler does not choose another intention in its place. A wait(...), blocking action or subgoal that depends on another intention can therefore stall the agent. A condition supplied by perception or an external action completion may still resume it, but all other intentions are delayed meanwhile.
The atomic flag is read from the plan that created the intention, not from every subgoal plan on its stack. Calling an atomic plan through an ordinary !subgoal() does not make a previously non-atomic intention atomic. Conversely, subgoals called within an atomic intention remain subject to that intention’s scheduling priority. A spawned !!goal() creates a separate intention, whose initial plan determines its atomic status.
Use a synchronized block when a particular shared update needs protection, especially in code called as a subgoal. Use atomic plans only when you intentionally need the whole intention to retain scheduling priority.
Choosing a mechanism
Requirement |
Use |
|---|---|
Protect a particular read/update sequence |
|
Protect every statement in a plan body |
|
Give a short intention uninterrupted scheduling priority over other intentions |
|
Coordinate different agents or external resources |
Messages, protocols or environment-side operations |
Synchronization works only when competing code uses the same lock. Atomic scheduling excludes other intentions more broadly, but neither mechanism freezes sensors, provides database-style transactions, or replaces a protocol for coordinating agents.