views:

206

answers:

6

Hey guys,

I'm wondering if this is the right approach to writing a thread-safe queue in C++?

template <class T>
class Queue
{
public:

Queue() {}

void Push(T& a)
{
    m_mutex.lock();
    m_q.push_back(a);
    m_mutex.unlock();
}

T& Pop()
{
    m_mutex.lock();
    T& temp = m_q.pop();
    m_mutex.unlock();
    return temp;
}

private:
    std::queue<t> m_q;
    boost::mutex m_mutex;
};

You get the idea... I'm just wondering if this is the best approach. Thanks!

EDIT: Because of the questions I'm getting, I wanted to clarify that the mutex is a boost::mutex

+4  A: 

From a threading-point of view, that looks about right for a simple, thread-safe queue.

You do have one problem, though: std::queue's pop() does not return the element popped from the queue. What you need to do is:

T Pop()
{
    m_mutex.lock();
    T temp = m_q.front();
    m_q.pop();
    m_mutex.unlock();
    return temp;
}

You don't want to return a reference in this case since the referenced element is being popped from the queue and destroyed.

You also need to have some public Size() function to tell you how many elements are in the queue (either that, or you'll need to gracefully handle the case where Pop() is called and there are no elements in the queue).

Edit: Though, as Eli Courtwright points out, you do have to be careful with the queue operations throwing exceptions, and using Boost is a good idea.

James McNellis
Polaris878
No, because the referenced object gets destroyed when pop() is called.
James McNellis
But why does it work lol? I just tried it out and was getting valid data back... which doesn't make sense.
Polaris878
If `T` has a trivial destructor, it may _look_ like that works, but it doesn't really work. When you call `m_q.pop()`, the front element is popped and its destructor is called.
James McNellis
Yeah after reading about it some more you are right... References still confuse me :) I'll have to eat the cost of the copy ctor.
Polaris878
+1  A: 

Depends what are your goals. Crafted in this manner, you'll have your "reader" client blocking your "writer" client. You might want to consider using a "condition" to avoid dead-locks etc.

jldupont
I'm not sure I see the deadlock in this code.
BobbyShaftoe
I didn't say there was a deadlock: I stated there was a least a limitation (that *could* yield to deadlock) depending on the usage pattern.
jldupont
What usage pattern? I plan on using this with a reader/writer (one of each)
Polaris878
Then do you intend to craft your "reader"? If you call "pop", it will block until.... ? your writer won't get a chance to write... unless you haven't showed us all the code you intend to use.
jldupont
I was not originally intending to block on pop. If I did, I would use a condition variable... I was originally going to call Empty() before I popped... however that is subject to race conditions. So, I have to rethink this again :( Thanks for bringing that up though.
Polaris878
+13  A: 

I recommend using the Boost threading libraries to assist you with this.

Your code is fine, except that when you write code in C++ like

some_mutex.lock();
// do something
some_mutex.unlock();

then if the code in the // do something section throws an exception then the lock will never be released. The Boost library solves this with its classes such as lock_guard in which you initialize an object which acquires a lock in its constructor, and whose destructor releases the lock. That way you know that your lock will always be released. Other languages accomplish this through try/finally statements, but C++ doesn't support this construct.

In particular, what happens when you try to read from a queue with no elements? Does that throw an exception? If so, then your code would run into problems.

When trying to get the first element, you probably want to check if something is there, then go to sleep if not and wait until something is. This is a job for a condition object, also provided by the Boost library, though available at a lower level if you prefer.

Eli Courtwright
Good catch for a very non-obvious problem.
BobbyShaftoe
As a C# programmer it seemed really obvious.
ChaosPandion
Likewise as a C++ programmer using exceptions.
John Zwinck
+1  A: 

The approach that your are trying to implement is a locking approach. It will work, except that if you use a plain system-provided "mutex" object, it's performance might turn out disappointing (its lock-unlock overhead is pretty high). It is hard to say whether it will be good or not, since we don't know what your performance requirements and expectations are.

Since the operations you perform in "locked" segments of your code are rather quick, it might make sense to use a spin-lock instead of a true mutex, or a combination of the two. This will give you much better performance. Then again, maybe your "mutex" already implements all that (no way to know, since you provided no details about what is actually hiding behind that name).

And finally, if you are happen to be looking for best performance, you might want to read up on lock-free synchronization approach, which is a completely different story. Lock-free methods are typically much more difficult to implement though.

AndreyT
+5  A: 

Herb Sutter wrote an excellent article last year in Dr. Dobbs Journal, covering all of the major concerns for a thread-safe, lock-free, single-producer, single-consumer queue implementation. (Which made corrections over an implementation published the previous month.)

His followup article in the next issue tackled a more generic approach for a multi-user concurrent queue, along with a full discussion of potential pitfalls and performance issues.

There are a few more articles on similar concurrency topics.

Enjoy.

greyfade
+1  A: 
Jerry Coffin