|
Wildmeshing Toolkit
|
The process-wide set of threads that task_group runs its tasks on. More...
#include <task_group.hpp>
Public Member Functions | |
| void | submit (std::unique_ptr< job > j) |
Run job on a worker. Throws (and queues nothing) if a needed worker cannot be started. | |
| bool | try_submit (std::unique_ptr< job > &j) |
| size_t | worker_count () |
| How many workers the pool has started. For tests. | |
Static Public Member Functions | |
| static worker_pool & | instance () |
Private Member Functions | |
| void | work () |
Static Private Member Functions | |
| static std::atomic< worker_pool * > & | current () |
Private Attributes | |
| std::mutex | m_mutex |
| std::condition_variable | m_cv |
| std::deque< std::unique_ptr< job > > | m_jobs |
| size_t | m_idle = 0 |
| workers that are free for a job: waiting for one, or about to | |
| size_t | m_spawning = 0 |
| workers being started, each owed to the job that started it | |
| size_t | m_workers = 0 |
| workers started | |
The process-wide set of threads that task_group runs its tasks on.
Every pass of the optimizers runs as a task_group of NUM_THREADS tasks, and task_group used to spawn a fresh std::thread per task and join it at the barrier. Thread creation itself is cheap; what it threw away was every thread_local the workers had built. Each pass started on threads that had never run an operation, so each of them rebuilt its per-thread polysolve solver – Solver::create validates its JSON parameters against the full spec, and at 16 threads on Thingi10K 103197 that was 12% of all thread time – and re-zeroed its ring-lock scratch, sized to the vertex capacity. A pass of a few milliseconds paid that start-up again every time.
The workers here are started once and kept, so a thread's caches survive from one pass to the next exactly as they already did in serial mode, where one thread runs everything.
SEMANTICS ARE THOSE OF A THREAD PER TASK. A task submitted while no worker is free for it gets a new worker; it never waits in a queue behind tasks that are still running. So a task may block on another task of the same or of another group, a task may run a task_group of its own, and several threads may submit at once, exactly as before – a fixed-size pool would deadlock on the first two. Every queued job is owed an idle worker, or a worker being started for it, and no other job can take that worker.
A worker counts as idle again before its task is reported finished, so a caller that starts its next group as soon as wait() returns finds every worker of the last one free. With one thread submitting – every caller in this repository – the pool therefore never grows past the largest number of tasks that were outstanding at once, which is NUM_THREADS.
The workers are never joined: they block on the condition variable for the life of the process, and the pool is deliberately leaked so that nothing in it is destroyed while a detached worker could still touch it.
A forked child (POSIX) inherits the pool's memory but none of its threads, and possibly its mutex in a locked state. The child therefore abandons it and starts a fresh pool on its first task – as it would have started fresh threads before the pool existed.
|
inline |
Queue j on a worker that is idle right now, as submit() would, and return true; or, if every idle worker is already owed to a queued job, return false with j left untouched. It never starts a worker.