Wildmeshing Toolkit
Loading...
Searching...
No Matches
task_group.hpp
1#pragma once
2
3#include <atomic>
4#include <cfenv>
5#include <condition_variable>
6#include <cstddef>
7#include <deque>
8#include <exception>
9#include <memory>
10#include <mutex>
11#include <optional>
12#include <thread>
13#include <type_traits>
14#include <utility>
15
16#if defined(__unix__) || defined(__APPLE__)
17#include <pthread.h>
18#define WMTK_WORKER_POOL_RESET_ON_FORK 1
19#endif
20
21namespace wmtk::threading {
22
23namespace detail {
24
27struct job
28{
29 virtual ~job() = default;
31 virtual void run() noexcept = 0;
34 virtual void finish() noexcept = 0;
35};
36
72{
73public:
74 static worker_pool& instance()
75 {
76#ifdef WMTK_WORKER_POOL_RESET_ON_FORK
77 static const bool registered = (pthread_atfork(nullptr, nullptr, &abandon_in_child), true);
78 (void)registered;
79#endif
80 auto& slot = current();
81 worker_pool* p = slot.load(std::memory_order_acquire);
82 if (p != nullptr) return *p;
83 // Created lazily, so that a forked child can start over. Threads racing to create it
84 // agree on one; the losers' pools have no workers yet and are simply deleted.
85 auto* fresh = new worker_pool();
86 if (slot.compare_exchange_strong(p, fresh, std::memory_order_acq_rel)) return *fresh;
87 delete fresh;
88 return *p;
89 }
90
92 void submit(std::unique_ptr<job> j)
93 {
94 {
95 std::unique_lock<std::mutex> lock(m_mutex);
96 // Every queued job must have an idle worker of its own: a worker that is busy may be
97 // running a task that waits for this one. A worker being started is already owed to
98 // the job that started it, so it does not count as free here: another thread taking
99 // it in the meantime would leave that job with no worker. Waking an idle worker that
100 // then finds the queue already drained by a worker finishing early is harmless; it
101 // waits again.
102 if (m_jobs.size() + m_spawning < m_idle) {
103 m_jobs.push_back(std::move(j));
104 lock.unlock();
105 m_cv.notify_one();
106 return;
107 }
108 ++m_spawning;
109 }
110 // No idle worker is free for this job. Start one BEFORE queueing the job, so that a
111 // failure to start it (std::system_error) leaves nothing behind for wait() to hang on.
112 // If an idle worker turned up in the meantime, one of the two just waits for the next job.
113 try {
114 std::thread([this] { work(); }).detach();
115 } catch (...) {
116 std::lock_guard<std::mutex> lock(m_mutex);
117 --m_spawning;
118 throw;
119 }
120 {
121 std::lock_guard<std::mutex> lock(m_mutex);
122 ++m_workers;
123 --m_spawning;
124 m_jobs.push_back(std::move(j));
125 }
126 m_cv.notify_one();
127 }
128
132 bool try_submit(std::unique_ptr<job>& j)
133 {
134 std::unique_lock<std::mutex> lock(m_mutex);
135 if (m_jobs.size() + m_spawning >= m_idle) return false;
136 m_jobs.push_back(std::move(j));
137 lock.unlock();
138 m_cv.notify_one();
139 return true;
140 }
141
144 {
145 std::lock_guard<std::mutex> lock(m_mutex);
146 return m_workers;
147 }
148
149private:
150 worker_pool() = default;
151
152 static std::atomic<worker_pool*>& current()
153 {
154 static std::atomic<worker_pool*> pool{nullptr};
155 return pool;
156 }
157
158#ifdef WMTK_WORKER_POOL_RESET_ON_FORK
162 static void abandon_in_child() { current().store(nullptr, std::memory_order_relaxed); }
163#endif
164
165 [[noreturn]] void work()
166 {
167 std::unique_lock<std::mutex> lock(m_mutex);
168 ++m_idle;
169 for (;;) {
170 m_cv.wait(lock, [this] { return !m_jobs.empty(); });
171 --m_idle;
172 std::unique_ptr<job> j = std::move(m_jobs.front());
173 m_jobs.pop_front();
174 lock.unlock();
175 // A task expects round-to-nearest, the mode the code that created its thread ran in.
176 // A reused worker starts in whatever the previous task left behind, and the exact
177 // predicates switch the rounding mode around their interval filters, so an exception
178 // in the middle of one would leak an upward mode into every later task on this thread.
179 std::fesetround(FE_TONEAREST);
180 j->run();
181 // Idle again BEFORE the task is reported finished: once it is, wait() may return and
182 // its caller start the next group, which must find this worker free rather than start
183 // another one. Until it waits again this worker does nothing that can block.
184 lock.lock();
185 ++m_idle;
186 lock.unlock();
187 j->finish();
188 j.reset();
189 lock.lock();
190 }
191 }
192
193 std::mutex m_mutex;
194 std::condition_variable m_cv;
195 std::deque<std::unique_ptr<job>> m_jobs;
196 size_t m_idle = 0;
197 size_t m_spawning = 0;
198 size_t m_workers = 0;
199};
200
201} // namespace detail
202
203// ---------------------------------------------------------------------------
204// task_group: replaces tbb::task_group. run() hands the task to a worker of the persistent
205// pool above (one worker per outstanding task, so the semantics are a thread per task);
206// wait() blocks until every task run since the last wait has finished, then rethrows the first
207// exception any of them threw.
208// ---------------------------------------------------------------------------
210{
211 std::mutex m_mutex;
212 std::condition_variable m_done;
213 size_t m_pending = 0;
214 std::exception_ptr m_eptr;
215
216public:
217 task_group() = default;
218 task_group(const task_group&) = delete;
219 task_group& operator=(const task_group&) = delete;
220
221 template <typename F>
222 void run(F&& f)
223 {
224 // The task is copied or moved into its job here, on the calling thread: a task whose copy
225 // or move throws makes run() throw -- as std::thread's constructor did -- before anything
226 // is counted or queued.
227 auto j = std::make_unique<task_job<std::decay_t<F>>>(*this, std::forward<F>(f));
228 {
229 std::lock_guard<std::mutex> lock(m_mutex);
230 ++m_pending;
231 }
232 try {
233 detail::worker_pool::instance().submit(std::move(j));
234 } catch (...) {
235 // Nothing was queued (see worker_pool::submit), so nothing will decrement this.
236 std::lock_guard<std::mutex> lock(m_mutex);
237 --m_pending;
238 throw;
239 }
240 }
241
250 template <typename F>
251 bool try_run(F&& f)
252 {
253 std::unique_ptr<detail::job> j =
254 std::make_unique<task_job<std::decay_t<F>>>(*this, std::forward<F>(f));
255 {
256 std::lock_guard<std::mutex> lock(m_mutex);
257 ++m_pending;
258 }
259 if (detail::worker_pool::instance().try_submit(j)) return true;
260 // Not queued, so nothing will report it finished.
261 std::lock_guard<std::mutex> lock(m_mutex);
262 --m_pending;
263 return false;
264 }
265
266 void wait()
267 {
268 std::unique_lock<std::mutex> lock(m_mutex);
269 m_done.wait(lock, [this] { return m_pending == 0; });
270 if (m_eptr) {
271 std::exception_ptr e = m_eptr;
272 m_eptr = nullptr;
273 lock.unlock();
274 std::rethrow_exception(e);
275 }
276 }
277
278 ~task_group()
279 {
280 std::unique_lock<std::mutex> lock(m_mutex);
281 m_done.wait(lock, [this] { return m_pending == 0; });
282 }
283
284private:
286 template <typename Fn>
287 class task_job final : public detail::job
288 {
289 task_group& m_group;
290 std::optional<Fn> m_fn;
291
292 public:
293 template <typename G>
294 task_job(task_group& group, G&& fn)
295 : m_group(group)
296 , m_fn(std::in_place, std::forward<G>(fn))
297 {}
298
299 void run() noexcept override
300 {
301 try {
302 (*m_fn)();
303 } catch (...) {
304 m_group.record(std::current_exception());
305 }
306 // Destroy the task's own state before it is reported finished: once it is, wait()
307 // may return and the caller may tear down whatever the task captured by reference.
308 m_fn.reset();
309 }
310
311 // After this the job touches nothing of the group's.
312 void finish() noexcept override { m_group.finish_one(); }
313 };
314
315 void record(std::exception_ptr e)
316 {
317 std::lock_guard<std::mutex> lock(m_mutex);
318 if (!m_eptr) {
319 m_eptr = std::move(e);
320 }
321 }
322
323 void finish_one()
324 {
325 // Notify while holding the lock, so wait() cannot return -- and the group cannot be
326 // destroyed -- until this thread has let go of the group's mutex.
327 std::lock_guard<std::mutex> lock(m_mutex);
328 if (--m_pending == 0) {
329 m_done.notify_all();
330 }
331 }
332};
333
334} // namespace wmtk::threading
The process-wide set of threads that task_group runs its tasks on.
Definition task_group.hpp:72
void submit(std::unique_ptr< job > j)
Run job on a worker. Throws (and queues nothing) if a needed worker cannot be started.
Definition task_group.hpp:92
bool try_submit(std::unique_ptr< job > &j)
Definition task_group.hpp:132
size_t worker_count()
How many workers the pool has started. For tests.
Definition task_group.hpp:143
A task of this group, queued on the pool. Move-only tasks are fine.
Definition task_group.hpp:288
void run() noexcept override
Runs the task and destroys its state. Never throws.
Definition task_group.hpp:299
void finish() noexcept override
Definition task_group.hpp:312
Definition task_group.hpp:210
bool try_run(F &&f)
As run(), but only on a worker of the pool that is idle right now. Returns false, having run nothing,...
Definition task_group.hpp:251
Definition task_group.hpp:28
virtual void finish() noexcept=0
virtual void run() noexcept=0
Runs the task and destroys its state. Never throws.