Threading
Threading is available on the native targets only: arm64, x86_64,
win64 and arm9. On xt6502 and m68k every one of these classes is a hard
#error, with no stub, so a threaded program cannot build and then run
single-threaded.
The API is small: Thread, Mutex (+ Guard), Cond, Sem, Atomic,
ThreadLocal, and Pool.forRange.
For a loop whose iterations are independent, a par block
is simpler: it splits the loop over every thread, or runs it on the GPU, with no
threads to manage.
For units of work that depend on one another, an
OperationQueue runs them on worker threads in
dependency order, from 0.7.
The part that is not in the library
Section titled “The part that is not in the library”Two threads that use one object race on its ARC refcount. A non-atomic increment that loses the race under-counts, and the object is freed while still in use. The crash can then appear far from the cause.
The back ends therefore emit the refcount update as an atomic read-modify-write. The decision is made per module: it is on when the module references the thread-creation runtime, which happens when the program spawns a thread. You do not need to request it.
-fthread-safe-arc and -fno-thread-safe-arc force it on or off when the
automatic decision is wrong. For example, a separately compiled library that a
threaded program will use needs the flag on.
Spawning
Section titled “Spawning”A thread body is a bound method (a callback), so the thread’s state is the
object the method belongs to. There is no void* context and no cast on entry.
class Worker : Object{ i32 id; i32 result; void run(void) { … } // this runs on the new thread}
Worker* a = Worker.withId((i32)2);Thread* ta = Thread.spawn(&a.run);ta.join();| Method | Effect |
|---|---|
Thread.spawn(&obj.body) | Start a thread running body. Returns a Thread*. |
join() | Block until the thread finishes. Returns false if it could not be joined. |
detach() | Let it run unjoined; its resources are released when it exits. |
isValid() | false if the thread failed to start. |
Thread.yield() | Hint to the scheduler. |
Thread.sleepMs(u32) | Sleep the calling thread. |
Mutex and Guard
Section titled “Mutex and Guard”Guard.on(m) locks immediately and unlocks when the guard is released at the end
of the scope. This includes an early return or a throw, which a bare
lock()/unlock() pair does not handle.
class Ledger : Object{ Mutex* lock; i32 balance; void init(void) { lock = new Mutex(); balance = (i32)0; }
void deposit(i32 amount) { Guard* g = Guard.on(lock); balance = balance + amount; } // unlocked here, whatever the exit}Mutex also has lock(), unlock() and tryLock() for when the scope does not
match the critical section.
Atomics
Section titled “Atomics”When the shared state is one word, use an Atomic: it needs no lock or scope,
and there is no unlock to forget.
Atomic* hits = Atomic.withValue((i32)0);hits.add((i32)5);hits.load();hits.store((i32)0);hits.compareAndSwap((i32)12, (i32)100); // true if it was 12Cond and Sem
Section titled “Cond and Sem”Cond is a condition variable paired with a Mutex. It waits for a predicate to
become true without spinning. Sem is a counting semaphore, for limiting use of
a resource.
Data parallelism
Section titled “Data parallelism”Pool.forRange(from, to, &obj.body) calls the body once per index, spread across
the available cores, and blocks until every index is done. The body is a
callback, so it can accumulate into the object it belongs to:
class Squares : Object{ Atomic* total; void init(void) { total = Atomic.withValue((i32)0); } void one(i32 i) { total.add(i * i); }}
Squares* sq = new Squares();Pool.forRange((i32)1, (i32)11, &sq.one); // 385Pool.forRangeWithThreads(from, to, body, n) sets the thread count.
Worked example
Section titled “Worked example”// threading.xc — Thread, Mutex, Guard, Atomic and Pool.#import "Stdio.xc"#import "Foundation.xc"#import "Thread.xc"#import "Mutex.xc"#import "Atomic.xc"#import "Pool.xc"
class Worker : Object{ i32 id; i32 result; void init(void) { id = (i32)0; result = (i32)0; } static Worker* withId(i32 n) { Worker* w = new Worker(); w.id = n; return w; }
void run(void) { i32 acc = (i32)0; for (i32 i = (i32)1; i <= (i32)1000; i = i + (i32)1) acc = acc + i * id; result = acc; }}
class Ledger : Object{ Mutex* lock; i32 balance; void init(void) { lock = new Mutex(); balance = (i32)0; }
void deposit(i32 amount) { Guard* g = Guard.on(lock); balance = balance + amount; }}
class Summer : Object{ Ledger* ledger; void init(void) { ledger = 0; } void addMany(void) { for (i32 i = (i32)0; i < (i32)500; i = i + (i32)1) ledger.deposit((i32)2); }}
class Squares : Object{ Atomic* total; void init(void) { total = Atomic.withValue((i32)0); } void one(i32 i) { total.add(i * i); }}
i32 main(void){ // ---- spawn and join ---- Worker* a = Worker.withId((i32)2); Worker* b = Worker.withId((i32)3); Thread* ta = Thread.spawn(&a.run); Thread* tb = Thread.spawn(&b.run); ta.join(); tb.join(); Stdio.printf("workers %ld %ld\n", a.result, b.result);
// ---- a mutex around shared state ---- Ledger* led = new Ledger(); Summer* s1 = new Summer(); s1.ledger = led; Summer* s2 = new Summer(); s2.ledger = led; Thread* t1 = Thread.spawn(&s1.addMany); Thread* t2 = Thread.spawn(&s2.addMany); t1.join(); t2.join(); Stdio.printf("balance %ld\n", led.balance);
// ---- atomics without a lock ---- Atomic* hits = Atomic.withValue((i32)0); hits.add((i32)5); hits.add((i32)7); Stdio.printf("atomic %ld, cas ok %d, after %ld\n", hits.load(), (i16)(hits.compareAndSwap((i32)12, (i32)100) ? 1 : 0), hits.load());
// ---- data parallelism ---- Squares* sq = new Squares(); Pool.forRange((i32)1, (i32)11, &sq.one); Stdio.printf("sum of squares 1..10 = %ld\n", sq.total.load());
return 0;}workers 1001000 1501500balance 2000atomic 12, cas ok 1, after 100sum of squares 1..10 = 385The balance is deterministic because deposit takes the lock: two threads each
make 500 deposits of 2.
Runtime
Section titled “Runtime”The runtime uses pthreads on macOS, raw clone + futex (Linux) or kernel32
(Windows) on the freestanding targets, and XTOS threads on arm9. Both macOS
runtimes share one runtime source.
On arm9 the kernel provides only thread lifecycle and a futex. Mutex, Cond
and Sem are built on it in user space, so an uncontended lock is
ldrex/strex and never enters the kernel. Two behaviours there differ from a
host:
- A faulting thread ends its whole process. The thread may have held shared locks and left shared state half-updated, so its siblings stop too.
cpuCount()returns 1, because XTOS owns one A9 core. A plainPool.forRangetherefore runs one worker. Set the count withPool.forRangeWithThreadswhen a workload needs more (the limit is 128 threads per process).
Known gap: the static-initialiser guard (__sinit_<Class>) is a
check-then-act, so two threads that touch the same class’s statics for the first
time at once can both run the initialiser.