asynchrony 2.3.1
Add asynchrony to your C++ applications using standard C++20
Loading...
Searching...
No Matches
simple_worker.hpp
1/*
2 asynchrony : Add asynchrony to your apps
3
4 BSD 3-Clause License
5
6 Copyright (c) 2021, Siddiq Software LLC
7 All rights reserved.
8
9 Redistribution and use in source and binary forms, with or without
10 modification, are permitted provided that the following conditions are met:
11
12 1. Redistributions of source code must retain the above copyright notice, this
13 list of conditions and the following disclaimer.
14
15 2. Redistributions in binary form must reproduce the above copyright notice,
16 this list of conditions and the following disclaimer in the documentation
17 and/or other materials provided with the distribution.
18
19 3. Neither the name of the copyright holder nor the names of its
20 contributors may be used to endorse or promote products derived from
21 this software without specific prior written permission.
22
23 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
24 AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
25 IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
26 DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
27 FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
28 DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
29 SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
30 CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
31 OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
32 OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
33 */
34
35#pragma once
36#include <chrono>
37#ifndef SIMPLE_WORKER_HPP
38#define SIMPLE_WORKER_HPP
39
40
41#include <iostream>
42#include <functional>
43#include <memory>
44#include <thread>
45#include <mutex>
46#include <shared_mutex>
47#include <deque>
48#include <semaphore>
49#include <stop_token>
50#include <exception>
51#include <source_location>
52#include <atomic>
53
54#if defined(_Linux_) || defined(__linux__) || defined(__linux) || (defined(__APPLE__) && defined(__MACH__))
55#include <pthread.h>
56#elif defined(_WIN32) || defined(WIN32) || defined(_WIN64) || defined(WIN64)
57#include <windows.h>
58#include <processthreadsapi.h>
59#endif
60
61#include "siddiqsoft/WaitableQueue.hpp"
62#include "siddiqsoft/RunOnEnd.hpp"
63#include "private/common.hpp"
64
65namespace siddiqsoft
66{
103 template <typename T, int Pri = 0>
104 requires((Pri >= -10) && (Pri <= 10)) && std::move_constructible<T>
106 {
107 std::atomic<bool> accepting_items {true};
108 std::atomic<bool> shutdown_initiated {false};
109 std::once_flag shutdown_invoked;
110
112 static constexpr std::chrono::milliseconds DEFAULT_WAIT_FOR_NEXT_ITEM_MS {1500};
113 static constexpr std::chrono::milliseconds DEFAULT_SHUTDOWN_DRAIN_MS {1000};
114
115 public:
117 simple_worker(const simple_worker&) = delete;
118
121
122
134 {
135#if defined(DEBUG) || defined(_DEBUG)
136 std::cerr << std::format("{} - Waiting for queue to be empty: {}\n", __func__, items.toJson().dump(2));
137#endif
138 // Performs a graceful shutdown (drains and kills the threads.)
139 shutdown();
140 }
141
142 bool shutdown(std::chrono::milliseconds timeout = DEFAULT_SHUTDOWN_DRAIN_MS)
143 {
144 bool shutdown_status {false};
145
146 std::call_once(
147 shutdown_invoked,
148 [&](bool& status, std::chrono::milliseconds& t) {
149 accepting_items.store(false, std::memory_order_release);
150#if defined(DEBUG)
151 std::cerr << std::format("worker shutdown started inside call_once.. asking for waitUntilEmpty...for {}ms\n", t.count());
152#endif
153
154 // Drain existing items and wait for the queue to be empty.
155 // Add a total deadline with buffer of 500ms extra..
156 auto deadline = std::chrono::steady_clock::now() + t + std::chrono::milliseconds(500);
157 auto isDrained = items.waitUntilEmpty(t);
158
159#if defined(DEBUG)
160 std::cerr << std::format("worker shutdown possible; isDrained: {}. size:{}\n", isDrained, items.size());
161#endif
162
163 // Notify the processor to shutdown (we should have no outstanding items.)
164 processor.request_stop();
165#if defined(DEBUG)
166 std::cerr << std::format("worker shutdown started inside call_once\n");
167#endif
168
169 if (processor.joinable()) {
170 processor.join();
171 status = isDrained;
172#if defined(DEBUG)
173 std::cerr << std::format("worker shutdown ok; isDrained: {}. size:{}\n", isDrained, items.size());
174#endif
175 }
176#if defined(DEBUG)
177 else {
178 std::cerr << std::format("worker shutdown failed; isDrained: {}. size:{}\n", isDrained, items.size());
179 }
180
181 std::cerr << "WARNING: Graceful shutdown timeout exceeded\n";
182#endif
183
184 status = isDrained; // Timeout occurred
185 },
186 shutdown_status,
187 timeout);
188 return shutdown_status;
189 }
190
193
196
212 simple_worker(std::function<void(T&&)> c)
213 : callback(c)
214 {
215 }
216
217
235 void queue(T&& item) noexcept(false)
236 {
237 if (!accepting_items.load(std::memory_order_acquire)) {
238 throw std::runtime_error("Worker is shutting down, cannot queue new items");
239 }
240
241 items.emplace(std::move(item));
242 queueCounter.fetch_add(1, std::memory_order_release);
243 }
244
245#if defined(NLOHMANN_JSON_VERSION_MAJOR)
267 auto toJson() const -> nlohmann::json
268 {
269 auto itemsSize = items.size();
270 auto itemsQueued = items.addCounter();
271 auto itemsPopped = items.removeCounter();
272 auto itemsOutstanding = itemsQueued - itemsPopped;
273
274 return {{"_typver", "siddiqsoft.asynchrony-lib.simple_worker/0.10"},
275 {"itemsSize", itemsSize},
276 {"queueCounter", queueCounter.load(std::memory_order_acquire)},
277 {"itemsQueued", itemsQueued},
278 {"itemsPopped", itemsPopped},
279 {"itemsOutstanding", itemsOutstanding},
280 {"threadPriority", Pri},
281 {"outstandingCallback", outstandingCallback.load(std::memory_order_acquire)},
282 {"waitInterval", DEFAULT_WAIT_FOR_NEXT_ITEM_MS.count()}};
283 }
284#endif
285
286 private:
288 std::once_flag flag_forceCleanupTerminate {};
289
292 std::atomic_uint outstandingCallback {0};
293
295 std::atomic_uint64_t queueCounter {0};
296
298 siddiqsoft::WaitableQueue<T> items {};
299
301 std::function<void(T&&)> callback;
302
319 std::jthread processor {[&](std::stop_token st) {
320#if defined(WIN64) || defined(_WIN64) || defined(WIN32) || defined(_WIN32)
321 // Set the thread priority if possible
322 if constexpr (Pri != 0) SetThreadPriority(GetCurrentThread(), Pri);
323#endif
324
325 while (!st.stop_requested()) {
326 try {
327 // The getNextItem performs the wait on the signal and if it expires, returns empty.
328 // If there is an item, it will get that item (minimizing move) and performs the pop
329 // and returns the item so we can invoke the callback outside the lock.
330 // We must ensure that the callback is nonempty!
331 if (auto item = items.tryWaitItem(DEFAULT_WAIT_FOR_NEXT_ITEM_MS); item && !st.stop_requested() && callback) {
332 // Delegate to the callback outside the lock
333 try {
334 // We get an optional<> and thus the use of the * to get the value if present..
335 callback(std::move(*item));
336 }
337 catch (const std::exception& ex) {
338 // We swallow exceptions from the callback to avoid thread termination and log it if needed.
339 std::cerr << std::format("Ignoring Exception in simple_worker callback: {} - inner\n", ex.what());
340 }
341 }
342 }
343 catch (const std::exception& ex) {
344 // We swallow exceptions from the callback to avoid thread termination and log it if needed.
345 std::cerr << std::format("Ignoring Exception in simple_worker callback: {} - outer\n", ex.what());
346 }
347 } // while ..continue until we're asked to stop
348#if defined(DEBUG)
349 std::cerr << std::format("WARNING: Abandon {} items processing due to stop request!\n", items.size());
350#endif
351 }};
352 };
353
354#if defined(NLOHMANN_JSON_VERSION_MAJOR)
365 template <typename T, int Pri = 0>
366 static void to_json(nlohmann::json& dest, const siddiqsoft::simple_worker<T, Pri>& src)
367 {
368 dest = src.toJson();
369 }
370#endif
371
372} // namespace siddiqsoft
373#endif // !SIMPLE_WORKER_HPP
simple_worker & operator=(simple_worker &&)=delete
Move assignment operator (deleted - workers are not movable).
simple_worker & operator=(const simple_worker &)=delete
Copy assignment operator (deleted - workers are not copyable).
simple_worker(simple_worker &&)=delete
Move constructor (deleted - workers are not movable).
simple_worker(std::function< void(T &&)> c)
Constructs a worker thread with the given callback.
void queue(T &&item) noexcept(false)
Queue a work item for processing.
static constexpr std::chrono::milliseconds DEFAULT_WAIT_FOR_NEXT_ITEM_MS
Default wait interval for the worker thread waiting on items.
simple_worker(const simple_worker &)=delete
Copy constructor (deleted - workers are not copyable).
~simple_worker()
Destructor - gracefully shuts down the worker thread.