When accessing an object from multiple threads with one access possibly changing the object, you need some from of synchronization. For the scenario described you probably want to have a queue class which does the necessary thread protection and signalling. Here is a simple implementation:
#include <mutex>
#include <condition_variable>
#include <deque>
template <typename T>
class queue
{
private:
std::mutex d_mutex;
std::condition_variable d_condition;
std::deque<T> d_queue;
public:
void push(T const& value) {
{
std::unique_lock<std::mutex> lock(this->d_mutex);
d_queue.push_front(value);
}
this->d_condition.notify_one();
}
T pop() {
std::unique_lock<std::mutex> lock(this->d_mutex);
this->d_condition.wait(lock, [=]{ return !this->d_queue.empty(); });
T rc(std::move(this->d_queue.back()));
this->d_queue.pop_back();
return rc;
}
};
The code uses C++ 2011 constructs but it can easily be changed to avoid their use and rather use C++ 2003 constructs instead except that these aren't standardized.
The key points are:
- A
std::mutex
is used to make sure only on thread accesses the queue at a time.
- When putting something into the queue a lock on the mutex is acquired and the object is inserted into the queue. Once the object is inserted the lock is automatically released and a condition variable is signaled.
- When extracting something from the queue a lock on the mutex is acquired and the thread waits for the queue to be non-empty using a condition variable: if there is something in the queue already, the condition will be
true
and wait()
returns immediately, otherwise the thread is put to sleep until it gets a signal at which point the condition is reevaluated. Note, that the condition may be evaluated multiple times because there may be spurious wake-up to the condition variable.
- The lambda captures its context by value: all it really captures is
this
; member variables cannot be captured directly because they are not part of the local context.
- The result from
pop()
is returned by value: the moment the lock gets release, the container may be changed, making it impossible to return objects by reference, even if they were put into a suitable location.
The main reason this is somewhat of a toy example is that it doesn't have a nice way to shut down the system: If a thread is blocked on the queue it waits until there is another object. A real implementation would have some way to signal that it is time to shut down, possibly throwing an exception from pop()
. Also, sometimes it is useful to have queue which doesn't force blocking to extract an object. Instead, it would have a function try_pop()
which acquires a lock, checks if the queue is non-empty and depending on the result extracts and object or signals failure. A function like this is easy to implement, though.