user112513312
user112513312

Reputation: 509

Pausing thread on empty queue without using spinlock

I have some code that works like this:

std::queue<int> a_queue;
bool exit = false;

void MainThreadFunc(int somedata)
{
    a_queue.push(somedata);
}

void WorkerThreadFunc()
{
    while (true)
    {
        if (exit)
            return;

        while (a_queue.empty());

        DoSomethingWithData(a_queue.front());
        a_queue.pop();
    }
}

The problem is that I get really high CPU usage, which appear to be a result of the spinlock when there's nothing for the worker to do. I attempted to use a mutex, but I it'd require the main thread locking it when there's nothing in the queue (which can't happen, of course). What alternatives are there to prevent this?

Upvotes: 0

Views: 2286

Answers (3)

Andrew Henle
Andrew Henle

Reputation: 1

std::queue is not thread-safe. As @Hurkyl posted, you need a lock if you want to use std::queue.

As coded, if multiple threads are waiting for data in the queue, they could all act on the same value from a_queue.fronf(), then call a_queue.pop() a number of times unrelated to the number of values pushed on the queue.

See Thread safety for STL queue

Upvotes: 0

user4842163
user4842163

Reputation:

Given the stated requirements where the cost of thread scheduler/context switch is too expensive, typically the best bet is to simply burn cycles as you do now to meet the tightest latency demands.

An atomic CAS spin/busy loop is basically a polling method, and as is commonly associated with polling, it has a tendency to hog CPU. Yet it doesn't pay the cost of the scheduler which you want to avoid here with that tight 16ms deadline to do everything you need to do and deliver a frame. It's typically your best bet to meet this kind of deadline.

With games where you have a lively world and constantly-animated content, there typically aren't lengthy idle periods where nothing is happening. As a result, it's generally considered quite acceptable for a game to be constantly utilizing CPU.

Probably a more productive question given your requirements is not how to reduce CPU utilization so much as to make sure the CPU utilization is going towards a good purpose. Typically a concurrent queue can offer a lock-free, non-blocking query to check to see if the queue is empty, which you already seem to have given this line:

while (a_queue.empty());

You might be able to sneak in some things to compute here while the queue is empty. This way you're not burning up cycles merely busy-waiting on the queue to become non-empty.

Again, typically the ideal answer to your question would involve a condition variable (which does depend on context switches and threads being woken up). It will typically be the fastest way to put the thread to sleep (reducing the CPU utilization) and have it woken up at the desired time, but given the tight latency requirements, it might be best to forget about the CPU utilization and focus more on making sure it's going towards a good purpose.

Upvotes: 0

Wang Weiyi
Wang Weiyi

Reputation: 86

The code below is what I learnt before somewhere else. It is a blocking queue implement. Threads can put elements to the queue safely, and if a thread attempts to take element from the queue when it is empty, it will be blocked until some other thread put elements to the queue. Hope it can help you

#include <queue>
#include <cassert>
#include <mutex>
#include <condition_variable>
#include <thread>
template<typename T>
class BlockingQueue
{
private:
    std::mutex _mutex;
    std::condition_variable _condvar;
    std::queue<T> _queue;
public:
    BlockingQueue(): _mutex(),_condvar(),_queue()
    {

    }
    BlockingQueue(const BlockingQueue& rhs) = delete;
    BlockingQueue& operator = (const BlockingQueue& rhs) = delete;

    void Put(const T& task)
    {
        {
            std::lock_guard<std::mutex> lock(_mutex);
            _queue.push(task);
        }
        _condvar.notify_all();
    }

    T Take()
    {
        std::unique_lock<std::mutex> lock(_mutex);
        _condvar.wait(lock,[this]{return !_queue.empty(); });
        assert(!_queue.empty());
        T front(std::move(_queue.front()));
        _queue.pop();

        return front;
    }

};

Upvotes: 1

Related Questions