This is a, I hope, an improved version of this code, limiting atomic usage (following @GSliepen, @G.Sliepen advice) (NB I'm limited to, at most C++17):
#include <algorithm>
#include <array>
#include <atomic>
#include <cassert>
#include <cmath>
#include <condition_variable>
#include <cstddef>
#include <cstdint>
#include <iostream>
#include <mutex>
#include <thread>
// #define LOG
// #define USEACTIVESLEEP
#ifdef USEACTIVESLEEP
#define MAYBEUNSUSED(var) static_cast<void>(var)
// functions to sleep for short period of times
// active wait but thread sleep has a too large overhead to allow for short
// delays
namespace {
// Iterations per nanosec
double gk_ItPerns;
void EstimateItPerns() noexcept {
auto start = std::chrono::steady_clock::now();
constexpr std::size_t NbIt{1000000};
for (std::size_t i = 0; i < NbIt; ++i) {
volatile std::size_t DoNotOptimize = 0;
MAYBEUNSUSED(DoNotOptimize);
}
auto end = std::chrono::steady_clock::now();
auto delay =
std::chrono::duration_cast<std::chrono::nanoseconds>(end - start)
.count();
gk_ItPerns = static_cast<double>(NbIt) / static_cast<double>(delay);
}
void ActiveSleep(double i_ns) noexcept {
std::size_t NbIt = static_cast<std::size_t>(i_ns * gk_ItPerns);
for (std::size_t i = 0; i < NbIt; ++i) {
volatile std::size_t DoNotOptimize = 0;
MAYBEUNSUSED(DoNotOptimize);
}
}
} // namespace
#endif
class CAsyncAlgo {
public:
using Data_t = std::size_t;
private:
static constexpr std::size_t mNbData = 1024;
std::size_t mWorkingIndex = 1;
std::size_t mBufferIndex = 0;
// type of data buffer
using DataBuffer_t = std::array<Data_t, mNbData>;
std::size_t mIndex = 0;
bool mHasData = false;
std::array<DataBuffer_t, 2> mSamples;
// Mutex for condition_variable and atomics
std::mutex mMutex;
// Condition variable used to wake up the working thread
std::condition_variable mWakeUp;
// To stop the worker
std::atomic<bool> mStop{false};
// Is an Algo instance running?
std::atomic<bool> mBusy{false};
// 1- Can an Algo instance be launched (for testing spurious wake-up)?
// not atomic because always accessed inside critical section
bool mReady{false};
// working thread
std::thread Worker;
// WorkLoad internals
// previous seen max value in buffer
Data_t mMaxVal = 0;
// number of processed data
Data_t mProcessed = 0;
private:
bool Stop() const noexcept {
// 2- no synch needed?
return (mStop.load(std::memory_order_relaxed));
}
bool Ready() const noexcept { return (mReady); }
void WaitForJob() {
std::unique_lock<std::mutex> lock(mMutex);
#ifdef LOG
std::cout << "entering waiting state " << std::boolalpha << mStop
<< std::endl;
#endif
// 3- std::memory_order_relaxed possible as locking mMutex will prevent
// it to be reordered before the previous Workload call
mBusy.store(false, std::memory_order_relaxed);
assert(lock.owns_lock());
mWakeUp.wait(lock, [this]() -> bool { return (Stop() || Ready()); });
assert(lock.owns_lock());
assert(mBusy || Stop());
mReady = false;
#ifdef LOG
std::cout << "waked up " << std::this_thread::get_id() << std::endl;
#endif
}
// Check if the working buffer is holding increasing successive integers
// from some point max value must be strictly greater than the one of the
// previous call {5,6,7,3,4} is valid if previous greatest value is strictly
// smaller than 7 {5,6,7,2,4} is invalid smallest value must also be
// strictly greater than mMaxVal as buffers do not overlap
void WorkLoad() {
Data_t Max = mSamples[mWorkingIndex][mNbData - 1];
Data_t Min = mSamples[mWorkingIndex][0];
for (std::size_t i = 1; i < mNbData; ++i) {
if (mSamples[mWorkingIndex][i] !=
mSamples[mWorkingIndex][i - 1] + 1) {
assert(mSamples[mWorkingIndex][i - 1] ==
(mSamples[mWorkingIndex][i] + mNbData - 1));
Max = mSamples[mWorkingIndex][i - 1];
Min = mSamples[mWorkingIndex][i];
}
}
assert(Max > mMaxVal);
assert(Min > mMaxVal);
mMaxVal = Max;
mProcessed += mNbData;
}
void MainLoop() {
while (!Stop()) {
WaitForJob();
if (Stop()) {
return;
}
WorkLoad();
}
}
public:
CAsyncAlgo() : Worker([this]() mutable -> void { MainLoop(); }) {}
void Push(Data_t const Sample, std::size_t) {
// writing one sample in current circular buffer
mSamples[mBufferIndex][mIndex] = Sample;
mIndex = (mIndex + 1) % mNbData;
if (mIndex == 0) {
// buffer is full
mHasData = true;
}
}
bool IsReady() {
if (mHasData && (mBusy.load(std::memory_order_acquire) == false)) {
return true;
}
return false;
}
void SubmitJob() {
#ifdef LOG
std::cout << "SubmitJob" << std::endl;
#endif
{
std::lock_guard<std::mutex> lock(mMutex);
mReady = true;
// 4- std::memory_order_relaxed because no synch needed, read only
// by this thread
mBusy.store(true, std::memory_order_relaxed);
std::swap(mWorkingIndex, mBufferIndex);
mIndex = 0;
mHasData = false;
}
mWakeUp.notify_one();
}
void Run(double const, double &) const {
// NOP
}
// destructor
///\details finishing computation and releasing resources
///\todo explicitely "close" computation before the end of life of the
/// object
~CAsyncAlgo() {
{
#ifdef LOG
std::cout << "closing" << std::endl;
#endif
std::lock_guard<std::mutex> lock(mMutex);
// 5- std::memory_order_relaxed: on unlocking may synchronise with
// the lock in wait in this case, the worker will see true
mStop.store(true, std::memory_order_relaxed);
}
mWakeUp.notify_one();
if (Worker.joinable()) {
#ifdef LOG
std::cout << "waiting for last run" << std::endl;
#endif
Worker.join();
#ifdef LOG
std::cout << "finished" << std::endl;
#endif
std::cout << "Processed " << GetNbProcessed() << " data"
<< std::endl;
}
}
std::size_t GetNbProcessed() { return mProcessed; }
};
static constexpr std::size_t NbSamples = 1000000;
int main() {
CAsyncAlgo Algo;
std::cout << std::this_thread::get_id() << std::endl;
#ifdef USEACTIVESLEEP
EstimateItPerns();
std::size_t acc{0};
#endif
for (std::size_t i = 0; i < NbSamples; ++i) {
#ifdef USEACTIVESLEEP
double period = 10000.; // ns
// manage data production frequency
auto start = std::chrono::steady_clock::now();
#endif
CAsyncAlgo::Data_t data =
static_cast<CAsyncAlgo::Data_t>(i + 1); // 0 is reserved
Algo.Push(data, i);
#ifdef USEACTIVESLEEP
auto end = std::chrono::steady_clock::now();
// no more synchro needed as only this thread is designed to launch a
// new computation
if (static_cast<double>(
std::chrono::duration_cast<std::chrono::nanoseconds>(end -
start)
.count()) < period) {
ActiveSleep(
period -
static_cast<double>(
std::chrono::duration_cast<std::chrono::nanoseconds>(end -
start)
.count()));
}
end = std::chrono::steady_clock::now();
acc = acc + static_cast<std::size_t>(
std::chrono::duration_cast<std::chrono::microseconds>(
end - start)
.count());
#endif
if (Algo.IsReady()) {
#ifdef LOG
#ifdef USEACTIVESLEEP
std::cout << "Ready " << i << " "
<< static_cast<double>(acc) /
(static_cast<double>(i) + 1.)
<< " us/Sample" << std::endl;
#endif
#endif
Algo.SubmitJob();
}
double res;
Algo.Run(3.14, res);
}
#ifdef USEACTIVESLEEP
std::cout << static_cast<double>(acc) / NbSamples << "us/Sample"
<< std::endl;
#endif
return 0;
}
Live One atomic has been replaced by a simple bool as it was only accessed in critical sections. The other where kept due to access out of critical sections but with less constraint memory order as, once again, when inside such sections, they provides enough synchronization and ordering constraints.
Separating buffers management from workflow would still have to be done.
Yet I'd just like to have feedback on remaining comments (1 to 5)
ActiveSleep
? What is the origin of the realtime requirement? Most cases when I thought spinning is required were solved with properly specifying the workflow with flow graphs and let task scheduler (both libraries like TBB and the OS scheduler) to do their job. If the worker is sleeping most of the time, the producer thread might as well do the tasks itself. \$\endgroup\$