Search code examples
c++atomicproducer-consumercircular-bufferlockless

Lockless circular buffer with single producer singular consumer


I have a consumer thread that must never lock nor allocate memory, and a producer thread that can. I want to implement a two place circular buffer to be able to provide data to the consumer thread from the producer, with the bound that whenever no new data is available to consume, the consumer just re-uses the already available data.

This is what I've come up with for now:

bool newDataAvailable = false;
bool bufferEmpty = true;

foo* currentData = new foo();
foo* newData = new foo();

void consumer() {
  while(true) {
    currentData->doSomething();
    if(newDataAvailable) {
      foo* tmp = currentData;

      // Objects are swapped so the old one can be reused without additional allocations
      currentData = newData;
      newData = tmp;
      newDataAvailable = false;
      bufferEmpty = true;
    }
  }
}

void producer() {
  while(true) {
    while(!bufferEmpty) { wait(); }
    newData->init();
    bufferEmpty = false;
    newDataAvailable = true;
  }
}

Is this naive implementation ok? I know reading and writing to variables can be non-atomic, so I should use an atomic storage, but those can cause locks. Is the use of atomic storage needed here? Also, I'd like to eliminate the active wait in the producer, and I thought I could use a std::condition_variable, but they require the use of mutexes and I cannot afford them.


Solution

  • Writing multi-threaded code that shares variables without using a mutex is very difficult to get right. see An Introduction to Lock-Free Programming, Lock Free Buffer.

    If you absolutely must avoid using mutexes, then i would highly recommend using a pre-made lock-free queue, like e.g. Boost.lockfree or MPMCQueue as a light non-boost alternative.

    I know reading and writing to variables can be non-atomic, so I should use an atomic storage, but those can cause locks.

    std::atomic is generally lock-free (doesn't use a mutex) for all primitive types (up to the native size of your cpu). You can check if std::atomic will use a mutex for a given type by calling std::atomic<T>::is_lock_free

    Is the use of atomic storage needed here?

    Yes, absolutely. You either need to use mutexes or atomics.

    Also, I'd like to eliminate the active wait in the producer, and I thought I could use a std::condition_variable

    When you can't use mutexes your only option is to use a spin lock. If it is allowed in your context, you could use std::this_thread::yield() in the spin lock to reduce CPU load. (however a mutex might be faster then)

    Edit: A potential solution with only 2 atomics would be:

    std::atomic<foo*> currentData = new foo();
    std::atomic<foo*> newData = new foo();
    
    void consumer() {
        foo* activeData = currentData;
        while (true) {
            activeData->doSomething();
            foo* newItem = currentData;
            if (newItem != activeData) {
                newData = activeData;
                activeData = newItem;
            }
        }
    }
    
    void producer() {
        while (true) {
            foo* reusedData = newData;
            if (!reusedData)
                continue;
            newData = nullptr;
            reusedData->init();
            currentData = reusedData;
        }
    }