-
Notifications
You must be signed in to change notification settings - Fork 0
Thread Safety
ffredyk edited this page Jul 23, 2026
·
1 revision
SQ# enforces safe concurrent access to mutable data. Thread safety is SQ#'s responsibility, not the scripter's. Three mechanisms: freeze/thaw, shared values, and ownership transfer.
Every mutable array and hashmap is owned by exactly one scheduler. Cross-scheduler access to mutable data throws an error.
Scheduler 1 Scheduler 2
_arr = [1,2,3] _arr select 0 ❌ ERROR — not owned by Scheduler 2
_arr pushBack 4 ✅ OK _frozen select 0 ✅ OK — frozen is immutable
freeze _arr ✅ OK thaw _frozen ✅ OK — creates new owned copy
_owner = scheduler _arr; // owner scheduler ID (-1 = none)
_isLocal = isSchedulerLocal _arr; // can I access this?_data = [1, 2, 3, 4, 5];
_frozen = freeze _data; // immutable copy
// _frozen properties:
scheduler _frozen; // -1 (no owner — globally readable)
isFrozen _frozen; // true
isSchedulerLocal _frozen; // true (readable from any scheduler)
// Cannot mutate:
_frozen pushBack 6; // ERROR — frozen arrayUse case: Share data between schedulers safely.
_frozen = freeze [1, 2, 3];
_mutable = thaw _frozen; // new mutable array owned by current scheduler
_mutable pushBack 4; // OKUse case: Receive frozen data and modify locally.
// Scheduler 1: prepare and share
_data = collectGameState();
_frozen = freeze _data;
[_frozen] spawnOn ["AI", {
params ["_shared"];
_local = thaw _shared; // mutable copy on AI scheduler
_local pushBack newAIUnit;
processAI(_local);
}];if (!isFrozen _arr) then {
_arr pushBack _newElement; // safe to mutate
};Atomic variables for lock-free concurrent counters and flags.
shared _counter = 0; // CAS-based atomic
shared _flag = false;_counter add 1; // atomic increment
_counter sub 2; // atomic decrement
_val = get _counter; // atomic read
// Compare-and-swap
_old = get _counter;
_success = _counter compareSwap [_old, _old + 1];
// true if swap succeeded, false if another fiber changed itshared _counter = 0;
_swapped = false;
while { !_swapped } do {
_old = get _counter;
_new = _old + 1;
_swapped = _counter compareSwap [_old, _new];
};
// Atomic increment doneshared _lock = 0;
// Acquire
while { !(_lock compareSwap [0, 1]) } do {
// spin — in production, add small sleep
};
// ... critical section ...
// Release
_lock set [nil, 0]; // single writer — no CAS neededPlain
setis NOT atomic on shared. Always usecompareSwapfor concurrent writes.
shared _counter = 0;
_counter add 1; // ✅ ATOMIC — correct
_counter = get _counter + 1; // ❌ RACE CONDITION — wrong!
// Between get and set, another fiber could change the valueTransfer mutable array ownership to another scheduler:
_arr = [1, 2, 3];
_arr sendTo 2; // array now owned by scheduler 2
// _arr is INVALID on this scheduler nowThe array's data is not copied — ownership is transferred. The source scheduler loses access.
| Mechanism | Use When | Copies Data? | Atomic? |
|---|---|---|---|
freeze / thaw
|
Sharing complex data | Yes (freeze copies) | Immutable |
shared + add/sub/compareSwap
|
Simple counters, flags | No (single value) | Yes |
sendTo |
Transferring ownership | No | Ownership change |
Need to share data between schedulers?
├── Simple number/flag? → shared + add/sub/compareSwap
├── Complex array/hashmap?
│ ├── One-time share? → freeze (sender) + thaw (receiver)
│ ├── Transfer ownership? → sendTo
│ └── Ongoing sharing? → freeze pattern with periodic updates
└── Don't share at all? → keep it local, no extra work
- Default to local: Most code runs on one scheduler. Don't over-engineer.
- Freeze for sharing: Always freeze mutable data before cross-scheduler use.
-
Shared for counters: Use
shared+add/subfor atomic increments. -
CAS for updates: Use
compareSwapwhen updating shared values conditionally. - One owner rule: Each mutable array/hashmap has exactly one owner.
- Concurrency — scheduler architecture, fibers, spawnOn
- freeze — create immutable snapshot
- thaw — create mutable copy
- shared — atomic variable declaration
- add — atomic add
- sub — atomic subtract
- compareSwap — atomic CAS
- sendTo — transfer ownership
- scheduler — get value owner
- isSchedulerLocal — ownership check
- Getting Started
- Language Guide
- Type System
- Control Flow
- Functions & Code
- Strings & Text
- Arrays & Collections
- Concurrency
- Promise System
- Thread Safety
- Host API & Embedding
- CLI Tool
- Bytecode Reference
- Multiplayer
- Syntax Sugar
- Optimization Guide
- Benchmarks
- count
- select
- pushBack
- append
- deleteAt
- deleteRange
- resize
- reverse
- sort
- find
- in
- forEach
- freeze
- thaw
- isFrozen
- currentScheduler
- clientOwner
- allSchedulers
- schedulerName
- schedulerExists
- schedulerBudget
- setSchedulerBudget
- fiberCount
- readyFiberCount
- waitingFiberCount
- schedulerLoad
- sendTo