thread_pool.h
Work that happens somewhere other than the two threads that matter.
#include <sonore/thread_pool.h>- 1type
- Guides: Platforms and formats
On this page
Design notes
The two threads it must not be
Reading a hundred-megabyte sample cannot happen on the AUDIO thread: it allocates, it blocks on a disk, and either of those is a dropout. It cannot happen on the UI thread either, because that one belongs to the host -- a plugin that spends four seconds in a message handler is a plugin that freezes the DAW, and the user blames the DAW.
So: a small pool, owned by the plugin, that neither of those threads waits on.
Why the default is two threads and not sixteen
A thread pool conventionally defaults to the number of cores, which is right for an application because there is one of it. A plugin is not one of it. A session with forty instances -- ordinary in a mix -- would create forty pools, and on a sixteen-core machine that is six hundred threads, all of them idle, all of them costing a stack and a scheduler slot.
And they would buy nothing. What a plugin does in the background is READ FILES. That is I/O-bound: two threads saturate a disk queue as well as sixteen do, and on a network share more threads make it slower. So the default is two, the cap is four, and a caller who genuinely has parallel CPU work can ask for more and say why.
Cancellation is not optional
A user who opens a folder of forty thousand samples and immediately opens a different one must not wait for the first scan. So every job is handed a flag and is expected to look at it; a job that ignores it is a job that delays close() for as long as it runs, and close() is called from a destructor the host is waiting on.
The other kind: a REALTIME pool
Built with RealtimeThreadHints, the pool is for the opposite job: helpers that render part of a block, under the audio deadline. Each worker makes itself a real-time thread (Mach's time-constraint policy, from the hints) and joins the audio workgroup the pool is given -- itself, on its own thread, which is the only way os_workgroup_join works -- before it takes its next job. See audio_workgroup.h for where a workgroup comes from: the standalone's device, an AU host's render context, and from CLAP, VST3 and LV2 nowhere, because those formats have no way to say.
Two ways in. setAudioWorkgroup() from any thread but the audio one: it takes the pool's lock and wakes every worker. offerAudioWorkgroup() FROM the audio thread, where a DSP's workgroupChanged() hook runs in an AU: wait-free, a retain and an atomic exchange, and the workers pick it up before their next job. What a realtime pool does NOT change is the queue: addJob() still takes a lock and may allocate for the job's closure, so a DSP that feeds it from the audio thread is trading that for the parallelism, knowingly.
Everything about workgroups and real-time scheduling is macOS. Elsewhere a realtime pool runs its jobs exactly as an ordinary one does, and workgroupState() says that nothing was joined and why. Compiled for macOS and never run there: no Mac in this loop (see audio_workgroup.h).
Types 1
A small set of worker threads, owned by a plugin or an application, for work that must not run on the audio thread or the UI thread -- reading files above all. Mirrors the reference's class of the same name, with a job as a function handed its stop flag. See the file comment for why the default is two threads (one on a machine with fewer than four cores) and for the realtime kind, built with RealtimeThreadHints.
addJob, cancelPendingJobs, waitForAll and the queries take a lock: any thread but the audio one (the file comment says what a realtime pool allows). Jobs start on the workers in the order queued and are expected to check shouldStop; close(), which the destructor calls, waits for the running ones to return, so a job that ignores the flag holds up whoever destroys the pool. A job must not close or destroy its own pool: that would join the thread it is running on.