8 Thread Rules
dxtr edited this page 2026-10-17 10:29:51 +02:00

Thread Rules

What may be shared

Every object of cl-threads may be shared between threads, because that is what they are for, with two exceptions.

Object Rules
job-pool Any thread may give it work, wait for work, and ask about it. Destroy it from a thread that is not one of its workers.
job-group Any thread may submit jobs to it and ask jobs-done-p. Only one thread at a time may wait for it with wait-for-jobs.
worker-local The object may be shared. Each value belongs to its worker while jobs run; read the values elsewhere only when no job that uses them is running.
thread Any thread may join one made by make-thread, any number of times, ask any thread its name or whether it has finished, and interrupt any thread.
lock Any thread may take it. Not recursive.
semaphore Any thread may signal it or wait on it.

What a job may do

A job runs on a worker thread. It may:

  • call parallel-for on its own pool, and submit to and wait for groups of its own pool. The worker runs the awaited jobs itself while it waits, so this never deadlocks unless the waits form a cycle;
  • use locks and semaphores, and make threads;
  • call worker-local-value for worker-locals of its pool.

It must not:

  • destroy its own pool, which signals threads-error;
  • wait on a semaphore for a signal that only another job of the same pool will give. A worker blocked on a semaphore runs nothing, so if every worker waits like this, the pool stops. Wait for the other job with its group instead, whose waiting runs it. Without fibers, which SBCL lacks, a scheduler cannot fix this in general;
  • rely on dynamic bindings of the thread that submitted it. Workers see global values, plus the pool's :bindings. See Job Pools.

Visibility

When a thread learns that a job has finished, it sees what the job wrote:

  • after parallel-for or wait-for-jobs returns;
  • after jobs-done-p returns true;
  • after join-thread returns, for a thread's writes;
  • after wait-on-semaphore returns true, for writes made before the matching signal-semaphore;
  • inside with-lock, for writes made inside earlier with-locks of the same lock.

Allocation

Operation Allocates
parallel-for Nothing, once the pool has warmed up.
submit-parallel-for on a reused group, then wait-for-jobs Nothing, once warmed up.
jobs-done-p, worker-index, worker-local-value, with-lock Nothing.
make-job-group, make-worker-local, make-thread, make-lock, make-semaphore The new object. Make these once, not per frame.
worker-local-values A fresh list.

The function you pass counts separately: a closure made each time allocates, and one made once does not. See Parallel For.

Queued jobs are kept in task records that the pool recycles. The pool makes a new one only when more jobs are queued at once than ever before, and keeps it, so after the first large burst queueing no longer allocates.

Underneath

cl-threads is built on SBCL's own threads. What it takes from SBCL is confined to one internal package, cl-threads/sys:

What How
Threads sb-thread:make-thread
Joining a thread sb-thread:join-thread, which wakes every thread that waits for the same thread
Whether a thread has ended sb-thread:thread-alive-p
Locks sb-thread:mutex
Semaphores sb-thread:semaphore
Waiting with a zero timeout sb-thread:try-semaphore, since SBCL's wait takes only a positive timeout
Signalling a count of 0 nothing, since SBCL's own signal takes a count of 1 or more
Interrupting a thread sb-thread:interrupt-thread, with interrupts enabled around the function
Clearing the stack a worker's job used sb-sys:scrub-control-stack, since SBCL's collector scans stacks conservatively
Processor count sysconf on Linux, GetActiveProcessorCount on Windows; unknown elsewhere