moodycamel::ConcurrentQueue

repository·master·Indexed 11 days ago

https://github.com/cameron314/concurrentqueue

A high-performance, industrial-strength lock-free concurrent queue for C++11. It provides a single-header implementation of `ConcurrentQueue<T>` for non-blocking operations and `BlockingConcurrentQueue<T>` for low-overhead blocking consumers. Features include bulk enqueue/dequeue, producer and consumer tokens for optimized performance, and exception safety.

Tokens
4.5K
Snippets
13
Records
18
Agent score
46%

What's inside concurrentqueue

  1. Implement a blocking Producer/Consumer model

    master

    Use BlockingConcurrentQueue<T> when consumers should wait for items to become available. Note that you must coordinate the shutdown (e.g., by tracking the number of promised elements) to prevent consumers from calling wait_dequeue and blocking forever after all producers have finished.

    BlockingConcurrentQueue<Item> q;
    const int ProducerCount = 8;
    const int ConsumerCount = 8;
    // Track how many items are expected in total
    std::atomic<int> promisedElementsRemaining(ProducerCount * 1000);
    
    // Producers
    for (int i = 0; i != ProducerCount; ++i) {
    	producers[i] = std::thread([&]() {
    		for (int j = 0; j != 1000; ++j) {
    			q.enqueue(produceItem());
    		}
    	});
    }
    
    // Consumers
    for (int i = 0; i != ConsumerCount; ++i) {
    	consumers[i] = std::thread([&]() {
    		Item item;
    		while (promisedElementsRemaining.fetch_sub(1, std::memory_order_relaxed) > 0) {
    			q.wait_dequeue(item);
    			consumeItem(item);
    		}
    	});
    }
  2. How producer and consumer tokens work

    master

    The queue supports both implicit and explicit methods for enqueueing and dequeueing.

    Implicit Methods

    When using implicit methods (e.g., enqueue(item)), the queue automatically allocates a thread-local producer sub-queue. These sub-queues are marked for reuse once the thread exits, though support for this varies by platform.

    Explicit Methods

    Explicit methods use user-provided tokens (e.g., enqueue(prod_token, item)).

    • Performance: Explicit methods are almost always faster.
    • Lifetime: Explicit producers are tied directly to their tokens' lifetimes.
    • Short-lived threads: If you are using the queue from many short-lived threads, it is highly recommended to use explicit producer tokens to avoid the overhead and potential issues of managing many implicit sub-queues.

    Ordering Note

    Because the queue uses independent sub-queues for producers, it is not linearizable. If two producers enqueue at the same time, there is no defined ordering between their elements when dequeued. However, elements from any individual producer will always come out in the order they were put in.

  3. Ensure exception safety with ConcurrentQueue

    master

    The queue is exception-safe and will not become corrupted if an element's constructor or assignment operator throws. The queue itself never throws (it returns false instead of throwing std::bad_alloc).

    Guarantees:

    • Enqueue: If an element's constructor throws, the operation is rolled back. Bulk enqueues use copy instead of move to ensure rollback integrity.
    • Dequeue: If an assignment operator throws during dequeue (single or bulk), the element is considered dequeued. It will be destructed, but cannot be recovered.

    Best Practices:

    • Annotate copy/move constructors and assignment operators with noexcept to avoid exception-checking overhead.
    • If using std::back_inserter for bulk operations, ensure the target container has enough capacity pre-reserved to avoid std::bad_alloc during insertion.
  4. Implement a simultaneous Producer/Consumer model

    master

    When producers and consumers run concurrently, you must coordinate their shutdown to ensure all items are processed. Using ConcurrentQueue<T> (non-blocking) requires manual coordination (e.g., using atomic counters) to signal when producers are finished so consumers know when to stop calling try_dequeue.

    ConcurrentQueue<Item> q;
    const int ProducerCount = 8;
    const int ConsumerCount = 8;
    std::atomic<int> doneProducers(0);
    std::atomic<int> doneConsumers(0);
    
    // Producers
    for (int i = 0; i != ProducerCount; ++i) {
    	producers[i] = std::thread([&]() {
    		while (produce) {
    			q.enqueue(produceItem());
    		}
    		doneProducers.fetch_add(1, std::memory_order_release);
    	});
    }
    
    // Consumers
    for (int i = 0; i != ConsumerCount; ++i) {
    	consumers[i] = std::thread([&]() {
    		Item item;
    		bool itemsLeft;
    		do {
    			// Check if producers are done before attempting final dequeue
    			itemsLeft = doneProducers.load(std::memory_order_acquire) != ProducerCount;
    			while (q.try_dequeue(item)) {
    				itemsLeft = true;
    				consumeItem(item);
    			}
    		} while (itemsLeft || doneConsumers.fetch_add(1, std::memory_order_acq_rel) + 1 == ConsumerCount);
    	});
    }
  5. Optimize queue performance with Tokens

    master

    The ConcurrentQueue can use per-producer and per-consumer storage to speed up operations via Tokens. Tokens are not thread-safe and should ideally be created once per thread (one ProducerToken and/or one ConsumerToken). While not strictly tied to a specific thread, they must only be used by a single producer or consumer at a time.

    Efficiency Hierarchy:

    1. Bulk methods with tokens
    2. Bulk methods without tokens
    3. Single-item methods with tokens
    4. Single-item methods without tokens

    In single-producer, multi-consumer scenarios, you can use try_dequeue_from_producer with a ProducerToken to reduce overhead.

    // Using tokens for faster enqueue/dequeue
    moodycamel::ConcurrentQueue<int> q;
    
    // Producer side
    moodycamel::ProducerToken ptok(q);
    q.enqueue(ptok, 17);
    
    // Consumer side
    moodycamel::ConsumerToken ctok(q);
    int item;
    q.try_dequeue(ctok, item);
    assert(item == 17);
  6. Preallocate memory using try_enqueue

    master

    The try_enqueue method never allocates memory; it returns false if the queue is full. To use this reliably, you must pre-allocate sufficient space in the constructor.

    Because the queue uses blocks (default size 32) and handles producers/consumers in a way that leaves partially filled blocks, simple sizing is insufficient.

    Sizing Formulas for $N$ elements:

    • Explicit producers (using tokens): (ceil(N / BLOCK_SIZE) + 1) * MAX_NUM_PRODUCERS * BLOCK_SIZE
    • Implicit producers (no tokens): (ceil(N / BLOCK_SIZE) - 1 + 2 * MAX_NUM_PRODUCERS) * BLOCK_SIZE
    • Mixed producers: ((ceil(N / BLOCK_SIZE) - 1) * (MAX_EXPLICIT_PRODUCERS + 1) + 2 * (MAX_IMPLICIT_PRODUCERS + MAX_EXPLICIT_PRODUCERS)) * BLOCK_SIZE

    Note: You can use the constructor overload that accepts $N$, MAX_EXPLICIT_PRODUCERS, and MAX_IMPLICIT_PRODUCERS directly to let the queue calculate the required size for you.

    Important: Even with correct sizing, try_enqueue might fail under high contention due to the queue's eventual consistency. Always handle the false return case (e.g., by looping).

  7. Install concurrentqueue using vcpkg

    master

    You can install moodycamel::ConcurrentQueue using the vcpkg dependency manager by following these steps:

    1. Clone the vcpkg repository.
    2. Bootstrap vcpkg.
    3. Integrate vcpkg with your environment.
    4. Install the concurrentqueue package.

    The vcpkg port is maintained by Microsoft and community contributors.

    git clone https://github.com/Microsoft/vcpkg.git
    cd vcpkg
    ./bootstrap-vcpkg.sh
    ./vcpkg integrate install
    vcpkg install concurrentqueue
  8. Install and use moodycamel::ConcurrentQueue

    master

    The moodycamel::ConcurrentQueue<T> is a high-performance, lock-free, thread-safe queue for C++. It is a single-header implementation, meaning you can use it by simply downloading and including the header file in your project.

    Requirements

    • C++11 or later.
    • A relatively recent compiler (e.g., VS2012+ or g++ 4.8).
    • Note: g++ 4.6 is not supported due to a known bug with std::atomic.

    Basic Usage

    Include concurrentqueue.h and use it like a standard templated queue. It is designed to be used concurrently from any number of threads.

    #include "concurrentqueue.h"
    
    moodycamel::ConcurrentQueue<int> q;
    q.enqueue(25);
    
    int item;
    bool found = q.try_dequeue(item);
    assert(found && item == 25);
  9. Basic usage of ConcurrentQueue

    master

    To use ConcurrentQueue<T> in a single-threaded context, use enqueue(item) to add elements and try_dequeue(item) to retrieve them. try_dequeue returns true if an item was successfully retrieved and false if the queue was empty.

    ConcurrentQueue<int> q;
    
    for (int i = 0; i != 123; ++i)
    	q.enqueue(i);
    
    int item;
    for (int i = 0; i != 123; ++i) {
    	q.try_dequeue(item);
    	assert(item == i);
    }
  10. Use ConcurrentQueue as an Object Pool

    master

    A ConcurrentQueue<T> can act as a thread-safe object pool. Threads can retrieve an object using try_dequeue. If the queue is empty, the thread can fall back to constructing a new object. When finished, threads can return the object to the pool using enqueue.

    class SomethingPool
    {
    public:
        Something getSomething()
        {
    	Something obj;
    	queue.try_dequeue(obj);
    	// If dequeue succeeded, obj is from the pool; otherwise it's default-constructed
    	return obj;
        }
    
        void recycleSomething(Something&& obj)
        {
    	queue.enqueue(std::move(obj));
        }
    };
  11. Implement a Threadpool Task Queue

    master

    For a threadpool, use BlockingConcurrentQueue<Task> to allow worker threads to sleep while waiting for work. Producers call enqueue to add tasks, and workers call wait_dequeue in a loop.

    BlockingConcurrentQueue<Task> q;
    
    // On any thread (Producer):
    q.enqueue(task);
    
    // On threadpool threads (Consumers):
    Task task;
    while (true) {
    	q.wait_dequeue(task);
    	// Process task...
    }