I am learning multithreading.
I took the example for ‘Producer-Consumer Problem ‘ in Operating Systems Concepts by Silberschartz, Galvin and Gagne, and wrote it in C++.
Here code in the book:
Producer
while (true) {
/*produce an item in next_produced*/
while (counter == BUFFER_SIZE)
; /*do nothing*/
buffer[in] = next_produced;
in = (in + 1) % BUFFER_SIZE;
counter++;
}
Consumer
while (true) {
/*produce an item in next_produced*/
while (counter == 0)
; /*do nothing*/
next_consumed = buffer[out];
out = (out + 1) % BUFFER_SIZE;
counter--;
}
The author explains why this is incorrect, because the increment or decrement of counter
is a ‘critical section’. So this will result in race conditions.
I wrote the following code in C++, with the critical section protected by a mutex.
#include <iostream>
#include <thread>
#include <mutex>
constexpr int BUFFER_SIZE = 5;
constexpr int CLIENT_BUFFER_SIZE = 10;
constexpr int NUM_DATA_TO_GENERATE = 7;
constexpr int END_OF_DATA = -1;
int buffer[BUFFER_SIZE];
int count = 0;
int clientBuffer[CLIENT_BUFFER_SIZE] = {};
std::mutex m;
int nextProduced()
{
static int i = 0;
static int counter = 0;
counter++;
if (counter == NUM_DATA_TO_GENERATE)
{
return END_OF_DATA;
}
const int arr[] = { 100, 200, 300, 400, 500 };
const int arrLen = sizeof(arr) / sizeof(int);
int next = arr[i];
i = (i + 1) % arrLen;
return next;
}
void producer()
{
int in = 0;
while (true)
{
while (count == BUFFER_SIZE);
int next = nextProduced();
buffer[in] = next;
in = (in + 1) % BUFFER_SIZE;
std::unique_lock<std::mutex> lck{ m };
count++;
if (next == END_OF_DATA)
{
break;
}
}
}
void consumer()
{
int out = 0;
int cbCounter = 0;
while (true)
{
while (count == 0);
int next = buffer[out];
out = (out + 1) % BUFFER_SIZE;
clientBuffer[cbCounter++] = next;
std::unique_lock<std::mutex> lck{ m };
count--;
if (next == END_OF_DATA)
{
break;
}
}
}
int main()
{
std::thread t1{ producer };
std::thread t2{ consumer };
t1.join();
t2.join();
std::cout << "Client Buffer" << std::endl;
for (int i = 0; i < CLIENT_BUFFER_SIZE; i++)
{
std::cout << clientBuffer[i] << " ";
}
std::cout << std::endl;
return 0;
}
The function nextProduced
simply produces a stream of data 100 , 200 , 300 , 400, 500, a fixed number of times after which it produces the data ‘END_OF_DATA’ signalling we have ended our data.
This code doesn’t work (gets into an infinite loop) when compiled with optimization O2 or O3 enabled. It works as expected if compiled without any optimization.
After some debugging with extensive use of print statements, at some point I got the indication that the loop
while (count == BUFFER_SIZE);
and/or
while (count == 0);
didn’t work. I changed the definition of count to volatile int count
and it worked.
Can someone explain this behavior ?
Why is this code wrong? If it is not wrong, why does it work if the shared variable is declared as volatile?