So I'm working on a job system where threads in a pool pull jobs off of a queue and run them, and each job can potentially queue other jobs. The queue is currently lockless, and the threads all wait on a semaphore that gets incremented as jobs are added rather than busy-waiting.
Now I'm trying to add the ability to queue jobs to be run on the main thread, which I think is as simple as having a second queue that only the main thread can pull from, but I'm having trouble figuring out how to keep the main thread from busy-waiting.
If I keep the current behavior of waiting on the semaphore the main thread will wake when jobs get added to the global queue, but not when they get added to the main thread's queue, which is a problem.
I could have a second semaphore that tracks jobs in the main thread's queue, but as far as I can tell Windows is the only platform that allows for waiting on multiple semaphores at once (via WaitForMultipleObjects
), and the system is supposed to be cross-platform.
I could probably do it with condition variables, but like I said the underlying queues are lockless, and std::mutex
is slow. I really don't like the idea of using dummy locks to begin with, especially if they have to be std::mutex
es.
I could also have the main thread ignore the global queue and only pull jobs from its own queue and wait on its own semaphore, that way I could just create one more worker thread and allow the OS to deschedule the main thread while it waits for work. But I don't like the idea of incurring context switches any time someone wants to use the main thread.
This seems like it should be pretty common behavior for any job system, but I've had no luck finding examples of it to study. Is there an approach I'm missing here?