LogIt++
Header-only, macro-first C++ logging library
Loading...
Searching...
No Matches
SingleThreadExecutor.hpp
Go to the documentation of this file.
1#pragma once
2#ifndef LOGIT_CPP_HEADER_LOGIT_CPP_LOGIT_DETAIL_SINGLETHREADEXECUTOR_HPP_INCLUDED
3#define LOGIT_CPP_HEADER_LOGIT_CPP_LOGIT_DETAIL_SINGLETHREADEXECUTOR_HPP_INCLUDED
4
7
8#include "QueuePolicy.hpp"
9#include <functional>
10#include <deque>
11#include <mutex>
12#include <atomic>
13#include <memory>
14#if !defined(__EMSCRIPTEN__) || defined(__EMSCRIPTEN_PTHREADS__)
15#include <condition_variable>
16#include <thread>
17#else
18#include <emscripten/emscripten.h>
19#endif
20
21namespace logit {
22namespace detail {
23
40#if !defined(__EMSCRIPTEN__) || defined(__EMSCRIPTEN_PTHREADS__)
42public:
54
59
64
66 void add_task(std::function<void()> task) {
67 if (!task) return;
68 std::unique_lock<std::mutex> lock(m_mutex);
69 if (m_stop.load(std::memory_order_acquire)) return;
70 if (m_max_queue_size > 0 && m_queue.size() >= m_max_queue_size) {
71 switch (m_overflow_policy) {
72 case QueuePolicy::DropNewest:
74 return;
75 case QueuePolicy::DropOldest:
76 if (!m_queue.empty()) {
77 m_queue.pop_front();
79 }
80 break;
81 case QueuePolicy::Block:
82 m_cv.wait(lock, [this]() {
83 return m_max_queue_size == 0 ||
84 m_queue.size() < m_max_queue_size ||
85 m_stop.load(std::memory_order_acquire);
86 });
87 if (m_stop.load(std::memory_order_acquire)) return;
88 break;
89 }
90 }
91 m_queue.push_back(std::move(task));
92 lock.unlock();
93 m_cv.notify_one();
94 }
95
100 void wait() {
101 std::unique_lock<std::mutex> lock(m_mutex);
102 m_cv.wait(lock, [this]() {
103 return m_queue.empty() &&
104 m_active_tasks.load(std::memory_order_relaxed) == 0;
105 });
106 }
107
113 void shutdown() {
114 bool notify_worker = false;
115 {
116 std::lock_guard<std::mutex> lock(m_mutex);
117 if (!m_shutdown_done.load(std::memory_order_acquire)) {
118 m_shutdown_done.store(true, std::memory_order_release);
119 m_stop.store(true, std::memory_order_release);
120 notify_worker = true;
121 }
122 }
123 if (notify_worker) {
124 m_cv.notify_all();
125 }
126 if (m_worker.joinable() && m_worker.get_id() != std::this_thread::get_id()) {
127 m_worker.join();
128 }
129 }
130
134 void set_max_queue_size(std::size_t size) {
135 {
136 std::lock_guard<std::mutex> lock(m_mutex);
137 if (m_stop.load(std::memory_order_acquire)) return;
138 m_max_queue_size = size;
139 }
140 m_cv.notify_all();
141 }
142
147 std::lock_guard<std::mutex> lock(m_mutex);
148 if (m_stop.load(std::memory_order_acquire)) return;
149 m_overflow_policy = policy;
150 }
151
153 std::size_t dropped_tasks() const noexcept {
154 return m_dropped_tasks.load(std::memory_order_relaxed);
155 }
156
158 void reset_dropped_tasks() noexcept {
159 m_dropped_tasks.store(0, std::memory_order_relaxed);
160 }
161
162private:
163 std::deque<std::function<void()>> m_queue;
164 mutable std::mutex m_mutex;
165 std::condition_variable m_cv;
166 std::thread m_worker;
167 std::atomic<bool> m_stop;
168 std::atomic<bool> m_shutdown_done;
169 std::size_t m_max_queue_size;
171 std::atomic<std::size_t> m_dropped_tasks;
172 std::atomic<std::size_t> m_active_tasks;
173
174 void worker_loop() {
175 for (;;) {
176 std::function<void()> task;
177 {
178 std::unique_lock<std::mutex> lock(m_mutex);
179 m_cv.wait(lock, [this]() {
180 return !m_queue.empty() || m_stop.load(std::memory_order_acquire);
181 });
182 if (m_stop.load(std::memory_order_acquire) && m_queue.empty()) {
183 break;
184 }
185 if (m_queue.empty()) continue;
186 task = std::move(m_queue.front());
187 m_queue.pop_front();
188 m_active_tasks.fetch_add(1, std::memory_order_relaxed);
189 }
190 m_cv.notify_all();
191
192 try {
193 task();
194 } catch (...) {
195 // Suppress exceptions from user tasks.
196 }
197
198 {
199 std::lock_guard<std::mutex> lock(m_mutex);
200 m_active_tasks.fetch_sub(1, std::memory_order_relaxed);
201 if (m_queue.empty() && m_active_tasks.load(std::memory_order_relaxed) == 0) {
202 m_cv.notify_all();
203 }
204 }
205 }
206 }
207};
208
209#else // single-threaded Emscripten: cooperative per-instance queue
219class SingleThreadExecutor {
220public:
221 SingleThreadExecutor()
222 : m_state(new State()) {}
223
224 ~SingleThreadExecutor() {
225 shutdown();
226 }
227
228 SingleThreadExecutor(const SingleThreadExecutor&) = delete;
229 SingleThreadExecutor& operator=(const SingleThreadExecutor&) = delete;
230 SingleThreadExecutor(SingleThreadExecutor&&) = delete;
231 SingleThreadExecutor& operator=(SingleThreadExecutor&&) = delete;
232
234 void add_task(std::function<void()> task) {
235 if (!task) return;
236 const std::shared_ptr<State> state = m_state;
237 bool schedule = false;
238
239 for (;;) {
240 bool drain_for_capacity = false;
241 {
242 std::lock_guard<std::mutex> lock(state->mutex);
243 if (state->shutdown_requested) return;
244
245 if (state->max_queue_size > 0 &&
246 state->queue.size() >= state->max_queue_size) {
247 switch (state->overflow_policy) {
248 case QueuePolicy::DropNewest:
249 ++state->dropped_tasks;
250 return;
251 case QueuePolicy::DropOldest:
252 if (!state->queue.empty()) {
253 state->queue.pop_front();
254 ++state->dropped_tasks;
255 }
256 break;
257 case QueuePolicy::Block:
258 drain_for_capacity = true;
259 break;
260 }
261 }
262
263 if (drain_for_capacity &&
264 state->max_queue_size > 0 &&
265 state->queue.size() >= state->max_queue_size) {
266 // Drain outside the lock to emulate producer backpressure
267 // without dropping the incoming task.
268 } else {
269 state->queue.push_back(std::move(task));
270 schedule = !state->scheduled;
271 state->scheduled = state->scheduled || schedule;
272 break;
273 }
274 }
275
276 drain_state(state);
277 }
278
279 if (schedule) {
280 schedule_drain(state);
281 }
282 }
283
287 void wait() {
288 drain_state(m_state);
289 }
290
295 void shutdown() {
296 const std::shared_ptr<State> state = m_state;
297 {
298 std::lock_guard<std::mutex> lock(state->mutex);
299 if (state->shutdown_requested) return;
300 state->shutdown_requested = true;
301 }
302 drain_state(state);
303 }
304
308 void set_max_queue_size(std::size_t size) {
309 std::lock_guard<std::mutex> lock(m_state->mutex);
310 if (m_state->shutdown_requested) return;
311 m_state->max_queue_size = size;
312 }
313
317 void set_queue_policy(QueuePolicy policy) {
318 std::lock_guard<std::mutex> lock(m_state->mutex);
319 if (m_state->shutdown_requested) return;
320 m_state->overflow_policy = policy;
321 }
322
323 std::size_t dropped_tasks() const noexcept {
324 return m_state->dropped_tasks.load(std::memory_order_relaxed);
325 }
326
327 void reset_dropped_tasks() noexcept {
328 m_state->dropped_tasks.store(0, std::memory_order_relaxed);
329 }
330
331private:
332 struct State {
333 State()
334 : max_queue_size(0)
335 , overflow_policy(QueuePolicy::Block)
336 , dropped_tasks(0)
337 , scheduled(false)
338 , shutdown_requested(false) {}
339
340 std::deque<std::function<void()>> queue;
341 std::mutex mutex;
342 std::size_t max_queue_size;
343 QueuePolicy overflow_policy;
344 std::atomic<std::size_t> dropped_tasks;
345 bool scheduled;
346 bool shutdown_requested;
347 };
348
349 std::shared_ptr<State> m_state;
350
351 static void schedule_drain(const std::shared_ptr<State>& state) {
352 std::shared_ptr<State>* token = new std::shared_ptr<State>(state);
353 emscripten_async_call(&SingleThreadExecutor::drain_thunk, token, 0);
354 }
355
356 static void drain_thunk(void* arg) {
357 std::unique_ptr<std::shared_ptr<State>> token(
358 static_cast<std::shared_ptr<State>*>(arg));
359 drain_state(*token);
360 }
361
362 static void drain_state(const std::shared_ptr<State>& state) {
363 for (;;) {
364 std::function<void()> task;
365 {
366 std::lock_guard<std::mutex> lock(state->mutex);
367 if (state->queue.empty()) {
368 state->scheduled = false;
369 break;
370 }
371 task = std::move(state->queue.front());
372 state->queue.pop_front();
373 }
374
375 try {
376 task();
377 } catch (...) {
378 // Suppress exceptions from user tasks.
379 }
380 }
381 }
382};
383#endif // !defined(__EMSCRIPTEN__) || defined(__EMSCRIPTEN_PTHREADS__)
384
385} // namespace detail
386} // namespace logit
387
388#endif // LOGIT_CPP_HEADER_LOGIT_CPP_LOGIT_DETAIL_SINGLETHREADEXECUTOR_HPP_INCLUDED
Per-instance single-thread executor for isolated async logging.
void wait()
Block until the queue is empty and no active tasks remain.
SingleThreadExecutor & operator=(SingleThreadExecutor &&)=delete
void set_max_queue_size(std::size_t size)
Change the maximum queue size (0 disables the limit).
SingleThreadExecutor & operator=(const SingleThreadExecutor &)=delete
~SingleThreadExecutor()
Destructor drains and joins the worker thread.
void add_task(std::function< void()> task)
Enqueue a task for asynchronous execution.
SingleThreadExecutor(SingleThreadExecutor &&)=delete
std::size_t dropped_tasks() const noexcept
Return the number of tasks dropped by the overflow policy.
void set_queue_policy(QueuePolicy policy)
Change the queue overflow policy.
std::deque< std::function< void()> > m_queue
SingleThreadExecutor(const SingleThreadExecutor &)=delete
void reset_dropped_tasks() noexcept
Reset the drop counter to zero.
SingleThreadExecutor()
Construct and immediately start the worker thread.
void shutdown()
Stop accepting new tasks, drain remaining, and join the worker thread.
QueuePolicy
Queue overflow handling policy used by TaskExecutor and SingleThreadExecutor.
@ Block
Producers wait until capacity is available.
The primary namespace for the LogIt++ library.