Unable to make an Producer-Consumer instance with list in c++

guys. I am learning about the Producer-Consumer Problem. My professor gave us an example code using a classical int array to share resources between the two threads. It works as expected, however, I wanted to try it using std:list class from C++ and it doesn’t work as expected. The consumer seems not to respect sem_wait(&full); so it tries to consume many times when there is nothing in the shared list.

Original code with array buffer

#include    <pthread.h>
#include    <semaphore.h>
#include    <stdlib.h>
#include    <stdio.h>
#include    <unistd.h>

#define    N 2
#define    TRUE 1

int    buffer[N], in = 0, out = 0;
sem_t  empty, full, mutexC, mutexP;

void *producer(void *arg) {

   while(TRUE) {
       sleep(rand()%5);

       sem_wait(&empty);
       sem_wait(&mutexP);
    
       buffer[in] = rand() % 100;
       printf("Producing buffer[%d] = %dn", in, buffer[in]);
       in= (in+1) % N;

       sem_post(&mutexP);
       sem_post(&full);
       
   }
}

void *consumer(void *arg) {

   while(TRUE) {
       
       sleep(rand()%5);
       sem_wait(&full);
       sem_wait(&mutexC);
       
       printf("Consuming buffer[%d] = %dn", out, buffer[out]);
       out = (out+1) % N;
       
       sem_post(&mutexC);
       sem_post(&empty);
       
   }
}

int main(int argc, char *argv[ ]) {
    pthread_t cons, prod;

    sem_init(&mutexC, 0, 1);
    sem_init(&mutexP, 0, 1);
    sem_init(&empty, 0, N);
    sem_init(&full, 0, 0);
    
    pthread_create(&prod, NULL, producer, NULL);
    pthread_create(&cons, NULL, consumer, NULL);
    
    pthread_exit(0);
}

My implementation with list:

#include    <pthread.h>
#include    <semaphore.h>
#include    <stdlib.h>
#include    <unistd.h>
#include    <iostream>
#include    <list>

#define    TRUE 1
#define    N 2

using namespace std;

list<int> sharedList;

sem_t  empty, full, mutexC, mutexP;

void *producer(void *arg) {

   while(TRUE) {
       
       sleep(rand()%5);

       sem_wait(&empty);
       sem_wait(&mutexP);
       int prod = rand() % 100;
       cout << "producing: " << prod << endl;
       sharedList.push_back(prod);

       sem_post(&mutexP);
       sem_post(&full);
       
   }
}

void *consumer(void *arg) {

   while(TRUE) {
      
       sleep(rand()%5);

       sem_wait(&full);
       sem_wait(&mutexC);
       
       if (!sharedList.empty()) {
           cout << "consuming: ";
           cout << sharedList.front() << endl;
           sharedList.pop_front();
       } else {
           cout << "not possible to consume" << endl;
       }
       sem_post(&mutexC);
       sem_post(&empty);
   }
}

int main(int argc, char *argv[ ]) {
    pthread_t cons, prod;

    sem_init(&mutexC, 0, 1);
    sem_init(&mutexP, 0, 1);
    sem_init(&empty, 0, N);
    sem_init(&full, 0, 0);
    
    pthread_create(&prod, NULL, producer, NULL);
    pthread_create(&cons, NULL, consumer, NULL);
    
    pthread_exit(0);
}

The – unexpected – logs from my implementation:

producing: 73
consuming: 73
not possible to consume
producing: 44
consuming: 44
producing: 9
producing: 65
consuming: 9
producing: 87
consuming: 65
consuming: producing: 29
87
not possible to consume
consuming: 29
producing: 9
producing: 60
consuming: 9
producing: 78

Can somebody explain to me what is happening? Thanks in advance!

Answer

In the first place, note that in both programs:

  • with only one producer ever contending for semaphore mutexP, that semaphore serves no useful purpose.
  • Likewise, with only one consumer ever contending for semaphore mutexC, that serves no useful purpose either.

Now, consider what purpose is served in your first program by initializing semaphore empty with value 2 instead of value 1: I presume that you will agree that doing so allows the producer to run concurrently with the consumer, one position ahead. This is ok because accesses to different array elements do not conflict.

But when your storage is a std::list instead of an array, it is not ok for two threads to manipulate it concurrently if one of them modifies it. There are several complementary ways to look at it, among them:

  • a std::list is a single object, access to which must be synchronized, whereas an array is merely an aggregate of its elements, not a distinct object of its own as far as synchronization requirements are concerned.

  • a std::list has members — maintaining the current list size, for example — that are used by all manipulations, access to which must be synchronized.

However you choose to look at it, you must ensure that the std::list cannot be accessed concurrently by two threads if one of those threads modifies it.