Skip to content

Thread Safety

ffredyk edited this page Jul 23, 2026 · 1 revision

Thread Safety

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.

Ownership Model

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

Ownership Commands

_owner = scheduler _arr;              // owner scheduler ID (-1 = none)
_isLocal = isSchedulerLocal _arr;      // can I access this?

Mechanism 1: freeze / thaw

freeze — Create Immutable Snapshot

_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 array

Use case: Share data between schedulers safely.

thaw — Create Mutable Copy

_frozen = freeze [1, 2, 3];
_mutable = thaw _frozen;       // new mutable array owned by current scheduler
_mutable pushBack 4;           // OK

Use case: Receive frozen data and modify locally.

Pattern: Share Between Schedulers

// 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);
}];

isFrozen

if (!isFrozen _arr) then {
    _arr pushBack _newElement;  // safe to mutate
};

Mechanism 2: shared (CAS Atomics)

Atomic variables for lock-free concurrent counters and flags.

Declaration

shared _counter = 0;           // CAS-based atomic
shared _flag = false;

Operations

_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 it

CAS Loop Pattern

shared _counter = 0;

_swapped = false;
while { !_swapped } do {
    _old = get _counter;
    _new = _old + 1;
    _swapped = _counter compareSwap [_old, _new];
};
// Atomic increment done

Simple Spinlock

shared _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 needed

Plain set is NOT atomic on shared. Always use compareSwap for concurrent writes.

vs Regular Add/Sub

shared _counter = 0;
_counter add 1;                     // ✅ ATOMIC — correct

_counter = get _counter + 1;        // ❌ RACE CONDITION — wrong!
// Between get and set, another fiber could change the value

Mechanism 3: sendTo (Ownership Transfer)

Transfer 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 now

The array's data is not copied — ownership is transferred. The source scheduler loses access.

Summary

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

Decision Guide

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

Best Practices

  1. Default to local: Most code runs on one scheduler. Don't over-engineer.
  2. Freeze for sharing: Always freeze mutable data before cross-scheduler use.
  3. Shared for counters: Use shared + add/sub for atomic increments.
  4. CAS for updates: Use compareSwap when updating shared values conditionally.
  5. One owner rule: Each mutable array/hashmap has exactly one owner.

See Also

SQ# Wiki

Home

Engine Docs

Migration

Commands

Value Constructors

Arithmetic

Comparison

Logic

Array

String

Math

Random

Type & Introspection

HashMap

Code Execution

Concurrency

Scheduler

Thread Safety

Error

Output

Time

Multiplayer

Compiler

Clone this wiki locally