4

So I'm looking at using a simple producer/consumer queue in C++. I'll end up using boost for threading but this example is just using pthreads. I'll also end up using a far more OO approach, but I think that would obscure the details I'm interested in at the moment.

Anyway the particular issues I'm worried about are

  1. Since this code is using push_back and pop_front of std::deque - it's probably doing allocation and deallocation of the underlying data in different threads - I believe this is bad (undefined behaviour) - what's the easiest way to avoid this?
  2. Nothing is marked volatile. But the important bits are mutex protected. Do I need to mark anything as volatile and if so what? - I don't think I do as I believe the mutex contains appropriate memory barriers etc., but I'm unsure.

Are there any other glaring issues?

Anyway heres the code:

#include <pthread.h>
#include <deque>
#include <iostream>

struct Data
{
  std::deque<int> * q;
  pthread_mutex_t * mutex;
};

void* producer( void* arg )
{
  std::deque<int> &q = *(static_cast<Data*>(arg)->q);
  pthread_mutex_t * m =  (static_cast<Data*>(arg)->mutex);

  for(unsigned int i=0; i<100; ++i)
  {
    pthread_mutex_lock( m );
    q.push_back( i );
    std::cout<<"Producing "<<i<<std::endl;
    pthread_mutex_unlock( m );
  }
  return NULL;
}

void* consumer( void * arg )
{
  std::deque<int> &q = *(static_cast<Data*>(arg)->q);
  pthread_mutex_t * m =  (static_cast<Data*>(arg)->mutex);

  for(unsigned int i=0; i<100; ++i)
  {
    pthread_mutex_lock( m );
    int v = q.front();
    q.pop_front();
    std::cout<<"Consuming "<<v<<std::endl;
    pthread_mutex_unlock( m );
  }  
  return NULL;
}

int main()
{
  Data d;

  std::deque<int> q;
  d.q = &q;

  pthread_mutex_t mutex;
  pthread_mutex_init( &mutex, NULL );
  d.mutex = & mutex;

  pthread_t producer_thread;
  pthread_t consumer_thread;

  pthread_create( &producer_thread, NULL, producer, &d );
  pthread_create( &consumer_thread, NULL, consumer, &d );

  pthread_join( producer_thread, NULL );
  pthread_join( consumer_thread, NULL );
}

EDIT:

I did end up throwing away this implementation, I'm now using a modified version of the code from here by Anthony Williams. My modified version can be found here This modified version uses a more sensible condition variable based approach.

Michael Anderson
  • 70,661
  • 7
  • 134
  • 187
  • 2
    The main problem is that you're asking us to evaluate a solution where you're going to rip up and throw away the underlying threading and then also rewrite the whole thing from scratch in a "far more OO" manner. I think that's known as premature evaluation :-) – paxdiablo Jul 19 '10 at 02:05
  • @paxdiablo: His specific questions do stand on their own merit. But +1 for the humorous terminology... – Amardeep AC9MF Jul 19 '10 at 02:12
  • @paxdiablo I'm just trying to avoid useless answers of the form "use objects" or "use boost". I'm well aware that I can run into other issues when I migrate my code - but the answers I obtain here will remain relevant in the modified code. – Michael Anderson Jul 19 '10 at 02:13
  • http://stackoverflow.com/questions/2363888 answers the allocation question. Summary matches what Amardeep and James McNellis have mentioned... with a minor proviso that as the standard doesn't mention threads, behaviour is implementation defined (i.e. undefined behaviuor) but all/most current implementations actually define it to be OK - so long as you link with the correct runtime libraries. – Michael Anderson Jul 19 '10 at 02:17
  • Myself, I would have just stated that "using objects or boost is not an option at this point so please don't suggest that". That does two things: (1) Makes people aware that you're not interested in those sort of answers; and (2) stops obnoxious yobbos called "pax" from giving you a hard time :-) Thanks for your clarifications. – paxdiablo Jul 19 '10 at 02:19

2 Answers2

2

Since this code is using push_back and pop_front of std::deque - it's probably doing allocation and deallocation of the underlying data in different threads - I believe this is bad (undefined behaviour) - what's the easiest way to avoid this?

As long as only one thread can modify the container at a time, this is okay.

Nothing is marked volatile. But the important bits are mutex protected. Do I need to mark anything as volatile and if so what? - I don't think I do as I believe the mutex contains appropriate memory barriers etc., but I'm unsure.

So long as you correctly control access to the container using a mutex, it does not need to be volatile (this is dependent upon your threads library, but it wouldn't be a very good mutex if it didn't provide a correct memory barrier).

James McNellis
  • 348,265
  • 75
  • 913
  • 977
1
  1. It is perfectly valid to allocate memory in one thread and free it in another if both threads are in the same process.

  2. Using a mutex to protect access to the deque should provide the correct memory access configuration.

EDIT: The only other thing to think about is the nature of the producer and consumer. Your synthesized example lacks some of the subtleties involved with a real implementation. For example, how will you synchronize the producer with the consumer if they are not operating at the exact same rate? You might want to consider using something like a pipe or an OS queue instead of a deque so that the consumer can block on read if there is no data ready to process.

Amardeep AC9MF
  • 18,464
  • 5
  • 40
  • 50
  • 1
    I think the edit probably falls into the area of a key point. The sample code presented will probably/possibly/maybe work, but there really is no guarantee that there will be anything at all in the queue when the consumer thread starts. If this happens, `queue.front()` and `queue.pop_front()` will have the bad kind of undefined behavior. – clstrfsck Jul 19 '10 at 02:44
  • @sprong : You're absolutely correct. Knowing when to stop and when to keep reading is a critical issue in this design.. and as you note its currently very broken... I've just been luck in the times I've run it that it didn't crash. Think I might have to close this and create a more detailed case, – Michael Anderson Jul 19 '10 at 03:54
  • @Michael: Several more points. You should unlock as soon as the queue operation is finished. Let's say the work to be done is symbolized by the `cout<<` statement, and that it takes time to execute. If the queue is locked for the entire duration, then the producer will be blocked from pushing into the queue until the consumer finishes an earlier item (that is, they can't do useful work concurrently). 2. Need a mechanism for stopping the consumer, i.e. there are no more items after this one. – rwong Oct 07 '10 at 05:35
  • @rwong - indeed, there was much wrong with the original code. I'm now using a much nicer solution, and have updated the OP to mention that solution. – Michael Anderson Oct 07 '10 at 06:11