Status: STABLE (disabled by default, enable with --enable-threads)
Enable with: ./configure --enable-threads
Portable across Linux, macOS, and Windows (see Portability.md and
Windows.md).
Two-level concurrency model:
-
Heavyweight threads (native OS threads) — each with its own VM registers, value stack, context stack, and GC buffer. Created via
%make-heavyweight-thread(opcode 70). Uses pthreads on POSIX/MinGW, native Win32 threads on MSVC (viaoak-thread.h). -
Lightweight processes (green threads) — Oaklisp-level continuations multiplexed within a single native thread via a cooperative scheduler with timer-driven preemption. Context switch via
pause()usingcall/cc.
Each native thread runs its own lightweight process scheduler. The
global scheduler queue (*scheduleQ*) is shared across all threads,
protected by a mutex.
Key files and #ifdef THREADS sections:
| File | Thread-related content |
|---|---|
threads.h |
Mutex declarations, THREADY() macro, TLS key, oak_threads_system_init() |
threads.c |
create_thread(), init_thread(), wait_for_gc(), set_gc_flag(), dynamic mutex init |
data.h |
Per-thread register macros, ALLOCATE_PROT spin-lock, GC buffer arrays |
loop.c |
Opcodes 67-72, POLL_GC_SIGNALS(), thread index init |
gc.c |
FORTHREADS macro, per-thread root scanning, GC coordination |
oaklisp.c |
Thread-0 TLS init, per-thread stack/register allocation |
stacks.c |
Thread index retrieval in init_stacks() |
config.h |
MAX_THREAD_COUNT (200), disables move-to-front under threads |
gc-ms.c |
Mark-sweep GC, TLABs, TLAB retire/refill, deferred objstart buffer |
gc-ms.h |
MS-GC and TLAB declarations, TLAB_SIZE (4096 words) |
Per-thread state (via TLS index_key):
register_array[my_index] VM registers (e_pc, e_env, e_bp, ...)
value_stack_array[my_index] Value stack
cntxt_stack_array[my_index] Context stack
gc_examine_buffer_array[my_index] GC worklist
tlab_cursor_array[my_index] TLAB bump pointer (USE_MARK_SWEEP only)
tlab_end_array[my_index] TLAB end pointer (USE_MARK_SWEEP only)
satb_buffer_array[my_index][] SATB write barrier log (USE_MARK_SWEEP only)
satb_count_array[my_index] SATB buffer entry count (USE_MARK_SWEEP only)
Global shared state:
Heap (new_space, old_space) Protected by alloc_lock
free_point, new_space.end Modified under alloc_lock
gc_pending, gc_ready[] GC coordination flags
objstart_bitmap, mark_bitmap GC bitmaps (USE_MARK_SWEEP only)
free_list_heads[] Segregated free lists (USE_MARK_SWEEP only)
GC coordination: one thread sets gc_pending, all others call
wait_for_gc() (blocking on gc_lock). Under USE_MARK_SWEEP, the
mark phase runs mostly concurrently with only brief STW pauses for
initial mark and remark; sweep runs lazily during allocation. Without
USE_MARK_SWEEP, GC is fully stop-the-world (copying collector).
Thread-related opcodes:
| Opcode | Name | Effect |
|---|---|---|
| 67 | ENABLE-ALARMS | timer_increment = 1 (enable preemption) |
| 68 | DISABLE-ALARMS | timer_increment = 0 (disable preemption) |
| 69 | RESET-ALARM-COUNTER | timer_counter = 0 |
| 70 | HEAVYWEIGHT-THREAD | Create native thread running given thunk; returns #t/#f |
| 71 | TEST-AND-SET-LOCATIVE | Atomic CAS on locative cell (mutex primitive) |
| 72 | THREAD-YIELD | oak_thread_yield() under THREADS, no-op otherwise |
| File | Content |
|---|---|
multi-em.oak |
Primitive ops: %make-heavyweight-thread, %load-process, %store-process, %test-and-set-locative, %thread-yield, %threads-enabled? |
multiproc.oak |
Queues, process descriptors, mutexes, scheduler, semaphores, futures |
multi-off.oak |
Guard variable %no-threading for cold-boot |
Synchronization primitives (all in multiproc.oak):
- Mutex: Spin-lock via
%test-and-set-locative. Yields between attempts via%thread-yield(opcode 72). - Semaphore: Mutex + waiting queue. Blocked processes enqueued,
woken on
signal. - Futures: Lazy evaluation with dependent process queue; auto-scheduled computation.
- Scheduler: Global
*scheduleQ*protected by mutex + alarm disable.pause()doescall/cccontext switch.
Thread infrastructure is not auto-initialized at warm boot. Auto-initialization was removed because it causes the compiler to deadlock: when mutex guards are active, compiling expressions that contain branch instructions (if/cond/or) re-enters mutex-protected code in the method-installation path.
Threading is initialized explicitly via --pthreads N, which calls
setup-initial-process-object in multiproc.oak. This in turn
calls init-thread-infrastructure, which:
- Creates a process descriptor and stores it in the process register
- Sets
%no-threadingto#f(enables per-process fluid bindings) - Initializes all guard-variable mutexes
- Redefines process initialization to assign unique PIDs
Then spawn-heavyweight-threads creates N-1 additional native threads.
On a non-THREADS emulator, %threads-enabled? returns nil and the
--pthreads option prints a warning and exits. Without --pthreads,
all guard-variable mutexes remain #f — zero overhead.
All known shared data structures are protected:
C emulator (protected by mutex or volatile):
signal_poll_flag—volatile sig_atomic_tfor signal safetyalloc_lock— protectsfree_point,new_space.end, and STORE-REG cases 14/15 inloop.cwp_lock— protectsref_to_wp()andrebuild_wp_hashtable()inweak.cdump_lock— protects DUMP-WORLD (case 73) and LOAD-WORLD (case 74) inloop.c
Oaklisp runtime (protected by guard-variable mutex pattern):
*symbol-table-mutex*—internandgensyminsymbols.oak*add-method-mutex*—%install-method-with-envinkernel1-install.oak*expression-table-mutex*—expression-tableandfancy-accessor-*lists inanonymous.oak*stream-primitive-mutex*—sp-alistcache instreams.oak*locale-mutex*—(setter variable-here?),(setter macro-here?),(setter frozen-here?)inlocales.oak*fluid-table-mutex*—add-to-current-fluid-bindingsinfluid.oak
All guard variables are #f during cold/warm boot (no mutex overhead)
and initialized to (make mutex) in init-thread-infrastructure
(multiproc.oak) when --pthreads is used.
Bootstrap constraint: The .oak source files for cold-world files
(symbols, kernel1-install, locales, fluid, anonymous, streams) contain
mutex guard code using if-guarded patterns (not when, which is
unavailable during cold compilation). However, freshly compiled cold
.oa files are bootstrap-incompatible — the installed compiler may
generate different bytecode for rest-arguments and format strings,
causing warm-boot hangs. Therefore all cold files always use prebuilt
.oa from prebuilt/src/world/. To update the prebuilt copies after
modifying cold .oak sources, first build a working world, use it to
recompile the modified files, and copy the results back to prebuilt/.
User responsibility: General user-created hash tables used concurrently need application-level locking.
Method cache: OP_TYPE_METH_CACHE is always enabled. Cache writes
use a seqlock pattern with OAK_ATOMIC_STORE_REF (release) /
OAK_ATOMIC_LOAD_REF (acquire). OP_METH_ALIST_MTF remains disabled
under threads since it mutates the shared alist structure.
- TLABs (under
USE_MARK_SWEEP + THREADS): Most allocations use a per-thread bump pointer with no lock. Only TLAB refill and large allocations acquirealloc_lock. - Concurrent GC (under
USE_MARK_SWEEP + THREADS): The mark phase runs mostly concurrently (only two brief STW pauses for initial mark and remark), and sweep runs lazily during allocation. The copying GC fallback remains STW but is only triggered for heap expansion.
See GC.md for full garbage collection documentation.
Tested with the full system world (oakworld.bin) across all four
build configurations:
| Config | Flags | 20-test suite | GC stress | Compile | Dump/reload |
|---|---|---|---|---|---|
| 1 | Default (copying GC) | 20/20 PASS | PASS | PASS | PASS |
| 2 | USE_MARK_SWEEP |
20/20 PASS | PASS | PASS | PASS |
| 3 | THREADS |
20/20 PASS | PASS | PASS | PASS |
| 4 | THREADS + USE_MARK_SWEEP |
20/20 PASS | PASS | PASS | PASS |
Additionally tested with -O0 debug build (config 4) and 5 repeated
runs — all 20/20 PASS with no non-deterministic failures.
Potential improvements, not required for current thread support:
- Generational hints: Track old->young pointers via a card table; skip scanning old-generation objects in minor collections
- Parallel marking: Multiple GC worker threads share the mark stack, reducing mark phase latency on multi-core systems
- Incremental compaction: Gradually relocate objects to reduce fragmentation without a full STW copying collection