1 Worker Storage
dxtr edited this page 2026-10-11 15:34:44 +02:00

Worker Storage

A worker-local holds one value for each worker of a pool. Each worker uses its own, so workers never contend for it and need no lock. Typical uses:

  • a running total or other accumulator for parallel-for, combined at the end;
  • a scratch buffer each worker reuses, so jobs allocate nothing;
  • a per-thread resource that must not be shared, such as a GPU command pool.
(make-worker-local pool function) → worker-local
(worker-local-value local) → value
(worker-local-values local) → list

Making one

make-worker-local calls function once per worker, with the worker's index, and keeps what it returns:

(defvar *scratch*
  (th:make-worker-local *pool* (lambda (index)
                                 (declare (ignore index))
                                 (make-array 1024 :element-type 'single-float))))

The function runs now, in the calling thread, not in the workers. Use the index if a value should depend on which worker it belongs to:

(th:worker-local-values (th:make-worker-local *pool* #'identity))
;; => (0 1 2 3), on a pool of four workers

Using it in jobs

worker-local-value returns the calling worker's value. It allocates nothing.

(th:parallel-for *pool* 0 (length *meshes*)
                 (lambda (start end)
                   (let ((buffer (th:worker-local-value *scratch*)))
                     (loop for i from start below end
                           do (build-mesh (aref *meshes* i) buffer)))))

Called from a thread that is not one of the pool's workers, including the thread that made it, it signals threads-error:

WORKER-LOCAL-VALUE was called outside the workers of #<JOB-POOL 4 workers>, which the value belongs to.

Collecting the values

worker-local-values returns every worker's value, in order of index, as a fresh list. Use it after the jobs are done, to combine results or release resources:

(th:with-job-pool (pool)
  (let ((totals (th:make-worker-local pool (lambda (index)
                                             (declare (ignore index))
                                             (make-array 1 :element-type 'double-float)))))
    (th:parallel-for pool 0 (length *numbers*)
                     (lambda (start end)
                       (let ((total (th:worker-local-value totals)))
                         (loop for index from start below end
                               do (incf (aref total 0) (aref *numbers* index))))))
    (reduce #'+ (th:worker-local-values totals) :key (lambda (total) (aref total 0)))))

Each total is a one-element array rather than a number, because the worker updates it in place: worker-local-value returns the value, not a place, so a number could not be incremented.

;; Releasing per-worker resources when done:
(mapc #'free-command-pool (th:worker-local-values *command-pools*))

Thread rules

The worker-local itself may be shared: every worker reads it. Each value belongs to its worker, and must only be used by that worker while jobs run. Read the values from another thread only when no job that uses them is running.

A job may run on any worker, and a waiting worker may run another job in the middle of its own. If a job holds its worker's value across a nested parallel-for or wait-for-jobs, another job may use the same value meanwhile, on the same worker. Finish with a scratch buffer before waiting on other jobs.