0

The producer, after each push into the queue, signals the consumer via conditionVar.notify_one().

However, the consumer wakes up after some random number of pushes (hence the subsequent notify_one()s) take place, sometimes 21 sometimes 2198, etc. Inserting a delay(sleep_for() or yield()) in the producer does not help.

How can I make this SPSC operate in lock-step?

How can I extend this example into multiple consumers(i.e. SPMC)?

void singleProducerSingleConsumer() {
    std::condition_variable conditionVar;
    std::mutex mtx;
    std::queue<int64_t> messageQueue;
    bool stopped = false;
    const std::size_t workSize = 4096;

    std::function<void(int64_t)> producerLambda = [&](int64_t id) {
        // Prepare a random number generator and push to the queue
        std::default_random_engine randomNumberGen{};
        std::uniform_int_distribution<int64_t> uniformDistribution{};

        for (auto count = 0; count < workSize; count++){
            //Always lock before changing state guarded by a mutex and condition_variable "conditionVar"
            std::lock_guard<std::mutex> lockGuard{ mtx };

            //Push a random number onto the queue
            messageQueue.push(uniformDistribution(randomNumberGen));

            //Notify the consumer
            conditionVar.notify_one();
            //std::this_thread::yield();
            /*std::this_thread::sleep_for(std::chrono::seconds(2));
            std::cout << "Producer woke " << std::endl;*/
        }
        //Production finished
        //Acquire the lock, set the stopped flag, inform the consumer
        std::lock_guard<std::mutex> lockGuard {mtx };

        std::cout << "Producer is done!" << std::endl;

        stopped = true;
        conditionVar.notify_one();
    };

    std::function<void(int64_t)> consumerLambda = [&](int64_t id) {

        do {
            std::unique_lock<std::mutex> uniqueLock{ mtx };
            //Acquire the lock only if stopped or the queue isn't empty
            conditionVar.wait(uniqueLock, [&]() {return stopped || !messageQueue.empty(); });

            //This thread owns the mutex here; pop the queue until it is empty
            std::cout << "Consumer received " << messageQueue.size() << " items" << std::endl;
            while (!messageQueue.empty()) {
                const auto val = messageQueue.front(); messageQueue.pop();
                std::cout << "Consumer obtained: " << val << std::endl;
            }
            uniqueLock.unlock();

            if (stopped) {
                //Producer has signaled a stop
                std::cout << "Consumer is done!" << std::endl;
                break;
            }

        } while (true);
    };

    std::thread consumer{ consumerLambda, 1 };
    std::thread producer{ producerLambda, 2 };
    consumer.join();
    producer.join();

    std::cout << "singleProducerSingleConsumer() finished" << std::endl;
}
Vectorizer
  • 1,076
  • 9
  • 24
  • No delay in your `for` loop _while you still hold the mutex_ can help your consumer, who is patiently waiting for that same mutex. Move your `yield` to just before `lockGuard` acquisition and you may see "fairer" behavior (more "continual" and less "batched"). I do. – pilcrow Dec 20 '19 at 22:23

1 Answers1

0

If I understand you correctly you want the consume each number produced by the producer before the producer produces the next number. That is fundamentally sequential execution, not concurrent execution. You can accomplish sequential execution most efficiently by simply having the producer pass the value to the consumer in normal function calls. The result would not be a good example of a Producer Consumer pattern.

Threads are scheduled by the Operating System in competition with all the other threads and processes executing on your computer at the same time. It is possible that your producer and consumer threads are operating on the same cpu core, meaning they must take turns executing as scheduled by the Operating System. Since the consumer cannot consume data until it is written by the producer it only makes sense that the producer will populate the messageQueue with several values during its first execution window before the consumer will be given time to consume the values in the messageQueue. The messageQueue will therefore be populated and de-populated in batches until the program completes.

Your solution should scale up to handling multiple consumers.

Jim Rogers
  • 4,822
  • 1
  • 11
  • 24
  • Thanks for your input. This code will evolve into an ASIC simulator; the producer will dispatch work to different subsystems (consumer threads). – Vectorizer Dec 20 '19 at 21:39