Question

The C++ memory model has relaxed atomics, which do not put any ordering guarantees on memory operations. Other than the mailbox example in C which I have found here:

http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1525.htm

Based on the motivating example in this paper:

http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2007/n2153.pdf

I was curious about other use cases for this type of synchronization mechanism.

Was it helpful?

Solution

A simple example that I see in my work frequently is a stats counter. If you want to count the number of times an event happens but don't need any sort of synchronization across threads aside from making the increment safe, using memory_order_relaxed makes sense.

static std::atomic<size_t> g_event_count_;

void HandleEvent() {
  // Increment the global count. This operation is safe and correct even
  // if there are other threads concurrently running HandleEvent or
  // PrintStats.
  g_event_count_.fetch_add(1, std::memory_order_relaxed);

  [...]
}

void PrintStats() {
  // Snapshot the "current" value of the counter. "Current" is in scare
  // quotes because the value may change while this function is running.
  // But unlike a plain old size_t, reading from std::atomic<size_t> is
  // safe.
  const size_t event_count =
      g_event_count_.load(std::memory_order_relaxed);

  // Use event_count in a report.
  [...]
}

In both cases, there is no need to use a stronger memory order. On some platforms, doing so could have negative performance impact.

OTHER TIPS

Event reader in this case could be connected to X11 socket, where frequency of events depends from a user actions (resizing window, typing, etc.) And if the GUI thread's event dispatcher is checking for events at regular intervals (e.g. due to some timer events in user application) we don't want to needlessly block event reader thread by acquiring lock on the shared event queue which we know is empty. We can simply check if anything has been queued by using the 'dataReady' atomic. This is also known as "Double-checked locking" pattern.

namespace {
std::mutex mutex;
std::atomic_bool dataReady(false);
std::atomic_bool done(false);
std::deque<int> events; // shared event queue, protected by mutex
}

void eventReaderThread()
{
    static int eventId = 0;
    std::chrono::milliseconds ms(100);
    while (true) {
        std::this_thread::sleep_for(ms);
        mutex.lock();
        eventId++; // populate event queue, e.g from pending messgaes on a socket
        events.push_back(eventId);
        dataReady.store(true, std::memory_order_release);
        mutex.unlock();
        if (eventId == 10) {
            done.store(true, std::memory_order_release);
            break;
        }
    }
}

void guiThread()
{
    while (!done.load(std::memory_order_acquire)) {
        if (dataReady.load(std::memory_order_acquire)) { // Double-checked locking pattern
            mutex.lock();
            std::cout << events.front() << std::endl;
            events.pop_front();
            // If consumer() is called again, and producer() has not added new events yet,
            // we will see the value set via this memory_order_relaxed.
            // If producer() has added new events, we will see that as well due to normal
            // acquire->release.
            // relaxed docs say: "guarantee atomicity and modification order consistency"
            dataReady.store(false, std::memory_order_relaxed);
            mutex.unlock();
        }
    }
}

int main()
{
    std::thread producerThread(eventReaderThread);
    std::thread consumerThread(guiThread);
    producerThread.join();
    consumerThread.join();
}
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top