asynchrony 2.3.1
Add asynchrony to your C++ applications using standard C++20
Loading...
Searching...
No Matches
periodic_worker.hpp
1/*
2 asynchrony-lib : 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 <siddiqsoft/RunOnEnd.hpp>
37#ifndef PERIODIC_WORKER_HPP
38#define PERIODIC_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 <utility>
51#include <exception>
52#include <source_location>
53#include <atomic>
54
55#if defined(_Linux_) || defined(__linux__) || defined(__linux) || (defined(__APPLE__) && defined(__MACH__))
56#include <pthread.h>
57#elif defined(_WIN32) || defined(WIN32) || defined(_WIN64) || defined(WIN64)
58#include <windows.h>
59#include <processthreadsapi.h>
60#endif
61
62#include "private/common.hpp"
63
64namespace siddiqsoft
65{
100 template <int Pri = 0>
101 requires((Pri >= -10) && (Pri <= 10))
103 {
105 static constexpr std::chrono::milliseconds DEFAULT_WAIT_FOR_NEXT_ITEM_MS {1500};
106
107 public:
110
112 auto& operator=(periodic_worker&) = delete;
113
116
118 auto& operator=(periodic_worker&&) = delete;
119
120
139 {
140#if defined(DEBUG) || defined(_DEBUG)
141 std::cerr << std::format(
142 "Shutting down periodic worker [{}] with outstanding callbacks [{}] and total invoke count [{}]\n",
143 threadName,
144 outstandingCallback.load(std::memory_order_acquire),
145 invokeCounter.load(std::memory_order_acquire));
146#endif
147
148 // This is critical step since we wait on the semaphore for a long time (keeps threads suspended) and if we do not
149 // decrease this interval then the shutdown will be quite delayed.
150 // Use atomic store with release semantics to safely modify invokePeriod from destructor
151 invokePeriod.store(std::chrono::microseconds(0), std::memory_order_release);
152 // Empty signal to get our thread to wake up
153 signal.release();
154
155#if defined(DEBUG) || defined(_DEBUG)
156 std::cerr << std::format("Signaled shutdown for periodic worker [{}], waiting for thread to join...\n", threadName);
157#endif
158
159 try {
160 // Notify the thread to stop.. and wait for a bit.. and then instead of joining we should just let the jthread
161 // destroy. Ask thread to shutdown and if joinable.. join.
162 processor.request_stop();
163 std::this_thread::sleep_for(std::chrono::milliseconds(100));
164 // if (processor.joinable()) processor.join();
165 }
166 catch (const std::exception& ex) {
167 std::cerr << std::format("Exception while shutting down periodic worker [{}]: {}", threadName, ex.what());
168 }
169
170#if defined(DEBUG) || defined(_DEBUG)
171 std::cerr << std::format("End of destructor for periodic worker [{}], waiting for thread to join...\n", threadName);
172#endif
173 }
174
194 void forceCleanupTerminate(const std::source_location& sl = std::source_location::current())
195 {
196 std::call_once(flag_forceCleanupTerminate, [&]() {
197 try {
198 // Notify the thread to stop.. and wait a bit before forceful termination
199 processor.request_stop();
200 std::this_thread::sleep_for(std::chrono::milliseconds(100));
201#if defined(_Linux_) || defined(__linux__) || defined(__linux) || (defined(__APPLE__) && defined(__MACH__))
202 auto nativeHandle = processor.native_handle();
203 std::cerr << std::format(
204 "forceCleanupTerminate - WARNING!! Calling native thread shutdown; only perform this when app is "
205 "ending! from: {}:{}",
206 sl.file_name(),
207 sl.line());
208 pthread_cancel(nativeHandle);
209 processor.detach();
210#elif defined(_WIN32) || defined(WIN32) || defined(_WIN64) || defined(WIN64)
211 auto nativeHandle = processor.native_handle();
212 std::cerr << std::format(
213 "forceCleanupTerminate - WARNING!! Calling native thread shutdown; only perform this when app is "
214 "ending! from: {}:{}",
215 sl.file_name(),
216 sl.line());
217 TerminateThread(nativeHandle, 0);
218 processor.detach();
219#endif
220 }
221 catch (const std::exception& ex) {
222 std::cerr << std::format("forceCleanupTerminate - Exception while shutting down worker: {}", ex.what());
223 }
224 });
225 }
226
244 periodic_worker(std::function<void()> c,
245 std::chrono::microseconds interval,
246 std::string name = {"anonymous-periodic-worker"})
247 : callback(std::move(c))
248 , invokePeriod(interval)
249 , threadName(std::move(name))
250 {
251 }
252
253
254#if defined(NLOHMANN_JSON_VERSION_MAJOR)
273 nlohmann::json toJson() const
274 {
275 using namespace std;
276
277 return {{"_typver"s, "siddiqsoft.asynchrony-lib.periodic_worker/0.10"s},
278 {"threadName", threadName},
279 {"outstandingCallbacks", outstandingCallback.load(std::memory_order_acquire)},
280 {"invokeCounter"s, invokeCounter.load(std::memory_order_acquire)},
281 {"threadPriority"s, Pri},
282 {"waitInterval"s, invokePeriod.load(std::memory_order_acquire).count()}};
283 }
284#endif
285
286 private:
288 std::once_flag flag_forceCleanupTerminate {};
289
292 std::atomic_uint outstandingCallback {0};
293
295 std::string threadName {"anonymous-periodic-worker"};
296
298 std::atomic_uint64_t invokeCounter {0};
299
302 std::counting_semaphore<1> signal {0};
303
311 std::atomic<std::chrono::microseconds> invokePeriod {std::chrono::milliseconds(1500)};
312
314 std::function<void()> callback;
315
334 std::jthread processor {[&](std::stop_token st) {
335#if defined(WIN64) || defined(_WIN64) || defined(WIN32) || defined(_WIN32)
336 // Set the thread priority if possible
337 if constexpr (Pri != 0) SetThreadPriority(GetCurrentThread(), Pri);
338#endif
339
340 while (!st.stop_requested()) {
341 try {
342 // This will wait until our period and return.
343 // We do not care about the return from try_acquire_for..
344 // We're using it as an efficient "wait" facility for period.
345 // Load invokePeriod atomically with acquire semantics
346 auto _ = signal.try_acquire_for(invokePeriod.load(std::memory_order_acquire));
347
348 if (!st.stop_requested()) {
349 auto decrementOutstandingCallback = siddiqsoft::RunOnEnd {[&] {
350 // Decrement outstanding callback
351 outstandingCallback.fetch_sub(1, std::memory_order_release);
352 }};
353
354 // Increment outstanding callback with release semantics
355 outstandingCallback.fetch_add(1, std::memory_order_release);
356 try {
357 // Delegate to the callback outside the lock
358 if (callback) callback();
359 invokeCounter.fetch_add(1, std::memory_order_release);
360 }
361 catch (const std::exception& ex) {
362 // We swallow exceptions from the callback to avoid thread termination and log it if needed.
363 std::cerr << std::format("Ignoring Exception (inner) in periodic_worker callback: {}", ex.what());
364 }
365 }
366 }
367 catch (const std::exception& ex) {
368 // We swallow exceptions from the callback to avoid thread termination and log it if needed.
369 std::cerr << std::format("Ignoring Exception (outer) in periodic_worker callback: {}", ex.what());
370 }
371 } // while ..continue until we're asked to stop
372 }};
373 };
374
375#if defined(NLOHMANN_JSON_VERSION_MAJOR)
385 template <int Pri = 0>
386 static void to_json(nlohmann::json& dest, const siddiqsoft::periodic_worker<Pri>& src)
387 {
388 dest = src.toJson();
389 }
390#endif
391
392} // namespace siddiqsoft
393#endif // PERIODIC_WORKER_HPP
auto & operator=(periodic_worker &)=delete
Copy assignment operator (deleted - workers are not copyable).
periodic_worker(std::function< void()> c, std::chrono::microseconds interval, std::string name={"anonymous-periodic-worker"})
Constructs a periodic worker thread.
void forceCleanupTerminate(const std::source_location &sl=std::source_location::current())
Force immediate termination of the worker thread.
periodic_worker(periodic_worker &&)=delete
Move constructor (deleted - workers are not movable).
~periodic_worker()
Destructor - gracefully shuts down the periodic worker thread.
periodic_worker(periodic_worker &)=delete
Copy constructor (deleted - workers are not copyable).
auto & operator=(periodic_worker &&)=delete
Move assignment operator (deleted - workers are not movable).
static constexpr std::chrono::milliseconds DEFAULT_WAIT_FOR_NEXT_ITEM_MS
Default wait interval for the worker thread.