diff --git a/samples.md b/samples.md index 52cf9b7e..4c6c9846 100644 --- a/samples.md +++ b/samples.md @@ -166,7 +166,8 @@ for (int i = 0; i != ConsumerCount; ++i) { The blocking version is different, since either the number of elements being produced needs to be known ahead of time, or some other coordination is required to tell the consumers when -to stop calling wait_dequeue (not shown here). This is necessary because otherwise a consumer +to stop calling wait_dequeue (see [draining a blocking queue](#draining-a-blocking-queue-when-producers-finish) +below for an example). This is necessary because otherwise a consumer could end up blocking forever -- and destroying a queue while a consumer is blocking on it leads to undefined behaviour. ```C++ @@ -200,6 +201,89 @@ for (int i = 0; i != ConsumerCount; ++i) { } ``` +## Draining a blocking queue when producers finish + +When the number of items is not known in advance, each producer can enqueue a +completion marker after its last item. The queue preserves each producer's order, +so once consumers have dequeued all producer markers, all work has been dequeued. +A single marker from an unrelated thread would not provide that guarantee: +there is no ordering between different producers. + +The last consumer to count a producer marker sends one stop marker per consumer. +These wake idle consumers without timeouts. Consumers still processing an item +finish it before reading a stop marker; joining all consumers waits for that +processing to complete before destroying the queue. + +This example requires positive producer and consumer counts. `produceItem(item)` +is an application function that returns false when that producer has no more +work; `consumeItem(item)` processes an item. The functions and `Item` operations +must not throw. The wrapper requires a default-constructible, copyable `Item`. +Enqueue failure terminates the example instead of losing work or a shutdown +marker; applications should choose their own failure policy. + +```C++ +struct Message { + enum Kind { Work, ProducerDone, Stop } kind; + Item item; +}; +BlockingConcurrentQueue q; +const int ProducerCount = 8; +const int ConsumerCount = 8; +std::thread producers[ProducerCount]; +std::thread consumers[ConsumerCount]; +std::atomic doneProducers(0); + +for (int i = 0; i != ConsumerCount; ++i) { + consumers[i] = std::thread([&]() { + Message message; + for (;;) { + q.wait_dequeue(message); + if (message.kind == Message::Stop) { + break; + } + if (message.kind == Message::Work) { + consumeItem(message.item); + } + else if (doneProducers.fetch_add(1, std::memory_order_acq_rel) + 1 == ProducerCount) { + for (int j = 0; j != ConsumerCount; ++j) { + if (!q.enqueue(Message{Message::Stop, Item{}})) { + std::terminate(); + } + } + } + } + }); +} +for (int i = 0; i != ProducerCount; ++i) { + producers[i] = std::thread([&]() { + Message message{Message::Work, Item{}}; + while (produceItem(message.item)) { + if (!q.enqueue(message)) { + std::terminate(); + } + } + // Use this producer's queue, after its final work item. + if (!q.enqueue(Message{Message::ProducerDone, Item{}})) { + std::terminate(); + } + }); +} +for (int i = 0; i != ProducerCount; ++i) { + producers[i].join(); +} +for (int i = 0; i != ConsumerCount; ++i) { + consumers[i].join(); +} +// All accepted items have been processed, and q can now be destroyed. +``` + +Each producer must send exactly one completion marker, using the same producer +thread (or explicit producer token) as its work, and must not enqueue more work +afterward. The completion counter combines observations from different consumers; +only the consumer reaching `ProducerCount` sends stop markers. Neither +`size_approx()` nor the relative ordering of different producers is used to +recognize completion. + ## Producer/consumer model (separate stages) ```C++ ConcurrentQueue q;