I need to properly synchronize access to some shared resource between a predefined number of worker threads (statically known via application config) and a predefined number of control-plane threads. The control-plane threads receive requests from the outside, and based on that potentially modify the shared resource. Worker threads simply run an infinite loop inside of which the shared resource is read only. To do this in a thread-safe way, and given the actual application use-case (network packet processing, multi data-plane threads and multi control-plane threads), it was decided to implement a "thread barrier" kind of pattern. Here's a snippet for how it's done, assuming the application is configured to spawn 2 worker threads and 2 control-plane threads:
std::atomic_bool barrier{};
std::atomic_uint32_t workers_at_barrier{};
// called by control-plane threads only!
void barrier_lock()
{
// optimized spinlock implementation: rigtorp.se/spinlock/
while (true)
{
if (!barrier.exchange(true, std::memory_order_acquire))
break;
while (barrier.load(std::memory_order_relaxed))
__builtin_ia32_pause();
}
assert(barrier);
// wait for ALL worker (data-plane) threads to arrive at the barrier!
while (workers_at_barrier.load() != 2);
assert(workers_at_barrier.load() == 2);
}
// called by control-plane threads only!
void barrier_unlock()
{
assert(barrier && workers_at_barrier.load() == 2);
barrier.store(false, std::memory_order_release);
// wait for ALL workers to get out of the barrier!
while (workers_at_barrier.load() != 0);
}
struct barrier_lock_guard
{
barrier_lock_guard()
{
barrier_lock();
}
~barrier_lock_guard()
{
barrier_unlock();
}
};
// control-plane threads receive some requests and handles them here
void handle_stuff()
{
// ... stuff
{
barrier_lock_guard blg;
// barrier should be set and all workers (2 in this case) should be waiting at the barrier for its release
assert(barrier && workers_at_barrier.load() == 2);
// ... writes to shared resource
}
// ... stuff
}
// called by worker threads only!
void wait_at_barrier()
{
// immediately return if barrier is not set
if (!barrier.load(std::memory_order_acquire))
return;
++workers_at_barrier;
// block at the barrier until it gets released
while (barrier.load(std::memory_order_acquire));
--workers_at_barrier;
}
// function run by the worker threads
void workers_stuff()
{
while (true)
{
wait_at_barrier();
// ... reads from shared resource
}
}
The problem is that the assert assert(barrier && workers_at_barrier.load() == 2); in handle_stuff() is getting hit. This occurs very very rarely, so there must be something wrong, and I'm trying to understand exactly what and where. Pretty sure though it has something to do with an incorrect use of std::memory_order. Any C++ atomics pro out there that can point me to the exact issue and what the proper fix would be? Thanks in advance.