Adding initial commit.
This commit is contained in:
parent
b7366e0a8d
commit
b5a684d85e
7
Doxyfile
Normal file
7
Doxyfile
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
GENERATE_HTML=yes
|
||||||
|
EXTRACT_ALL=yes
|
||||||
|
RECURSIVE=yes
|
||||||
|
INPUT=../include
|
||||||
|
DISABLE_INDEX = YES
|
||||||
|
GENERATE_TREEVIEW = YES
|
||||||
|
PROJECT_NAME=dining-philosophers
|
186
README.md
186
README.md
@ -1,2 +1,186 @@
|
|||||||
# producer-consumer
|
# Producer-Consumer Problem Solution.
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
This is a very common problem in multithreading, one or more threads
|
||||||
|
generate data in a buffer and one or more threads have to process
|
||||||
|
that data.
|
||||||
|
|
||||||
|
This solution is very useful when for example listening for
|
||||||
|
network connections in a multithreaded manner, various threads
|
||||||
|
wait for client requests while a pool of consumer threads
|
||||||
|
dispatch those requests.
|
||||||
|
|
||||||
|
## The full example
|
||||||
|
|
||||||
|
Just because the solution is in this case a single file
|
||||||
|
I decided to copy it in the README.md so you do
|
||||||
|
not have to navigate by the project, I could not
|
||||||
|
unfortunately do this with the philosophers having
|
||||||
|
a dinner.
|
||||||
|
|
||||||
|
|
||||||
|
```C++
|
||||||
|
#include <memory>
|
||||||
|
#include <iostream>
|
||||||
|
#include <thread>
|
||||||
|
#include <semaphore>
|
||||||
|
#include <string>
|
||||||
|
#include <thread>
|
||||||
|
#include <list>
|
||||||
|
|
||||||
|
void
|
||||||
|
argumentAsNumber(int argc, char **argv, int numberOfArgument, std::string nameOfArgument, int *output);
|
||||||
|
|
||||||
|
// The buffer size can be changed, the bigger it
|
||||||
|
// is, the less the producer threads would
|
||||||
|
// have to wait for a slot in the event of
|
||||||
|
// consumers being unable to keep up with
|
||||||
|
// the generated values.
|
||||||
|
const int sizeBuffer = 10;
|
||||||
|
|
||||||
|
// Handy typedefs to reduce the line noise.
|
||||||
|
// This is very useful for shared_ptr.
|
||||||
|
typedef std::list<std::jthread> ListOfThreads;
|
||||||
|
typedef std::shared_ptr<int[sizeBuffer]> Buffer;
|
||||||
|
typedef std::shared_ptr<int> BufferAllocation;
|
||||||
|
typedef std::counting_semaphore<sizeBuffer> ProducerConsumerSemaphore;
|
||||||
|
typedef std::shared_ptr<ProducerConsumerSemaphore> ProducerConsumerSemaphorePtr;
|
||||||
|
typedef std::shared_ptr<std::mutex> MutexPtr;
|
||||||
|
|
||||||
|
void
|
||||||
|
producerThread(Buffer buffer, BufferAllocation bufferAllocation,
|
||||||
|
ProducerConsumerSemaphorePtr producerAvailableSlots,
|
||||||
|
ProducerConsumerSemaphorePtr consumerAvailableSlots,
|
||||||
|
MutexPtr bufferMutex) {
|
||||||
|
// We are going to produce in the firsts iterations
|
||||||
|
// values from 0 to 50 and then values from
|
||||||
|
// 10 to 50.
|
||||||
|
int result = 0;
|
||||||
|
while (true) {
|
||||||
|
if (result > 50) {
|
||||||
|
result = 10;
|
||||||
|
}
|
||||||
|
// We reduce the available slots for producers.
|
||||||
|
// If we cant this would wait.
|
||||||
|
producerAvailableSlots->acquire();
|
||||||
|
{
|
||||||
|
// We lock the buffer to avoid other
|
||||||
|
// threads to mangle when we are
|
||||||
|
// still writing.
|
||||||
|
std::lock_guard<std::mutex> lk{*bufferMutex};
|
||||||
|
// We set the last element of the buffer
|
||||||
|
// to be the generated value.
|
||||||
|
buffer[*bufferAllocation] = result;
|
||||||
|
// We increment the buffer size, although I
|
||||||
|
// do not know if this is needed to be commented.
|
||||||
|
(*bufferAllocation)++;
|
||||||
|
}
|
||||||
|
// Now there is one more available slot to be consumed.
|
||||||
|
consumerAvailableSlots->release();
|
||||||
|
result++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
consumerThread(Buffer buffer, BufferAllocation bufferAllocation,
|
||||||
|
ProducerConsumerSemaphorePtr producerAvailableSlots,
|
||||||
|
ProducerConsumerSemaphorePtr consumerAvailableSlots,
|
||||||
|
MutexPtr bufferMutex) {
|
||||||
|
// We keep consuming forever in every consumer thread.
|
||||||
|
while (true) {
|
||||||
|
// This attempt to release the consumerAvailableSlots semaphore.
|
||||||
|
// It will wait if the operation cannot be done because
|
||||||
|
// it is already 0.
|
||||||
|
consumerAvailableSlots->acquire();
|
||||||
|
{
|
||||||
|
// We block the buffer and the variable containing its
|
||||||
|
// size so no other thread attempts to write or read there
|
||||||
|
// while we are doing this.
|
||||||
|
std::lock_guard<std::mutex> lk{*bufferMutex};
|
||||||
|
// We read the first value to create a first in first out.
|
||||||
|
// If we do not do this and the producer threads
|
||||||
|
// are more than the consumers or producing values
|
||||||
|
// way faster than they can be processed we would
|
||||||
|
// end with values that are never proccesed.
|
||||||
|
int result = buffer[0];
|
||||||
|
// We move the buffer -1 position each element
|
||||||
|
// so buffer[1] is now buffer[0]
|
||||||
|
for (int i = 1; i < *bufferAllocation; i++) {
|
||||||
|
buffer[i-1] = buffer[i];
|
||||||
|
}
|
||||||
|
std::cout << "Read " << result << " from the pool." << std::endl;
|
||||||
|
// We decrement the buffer allocated size.
|
||||||
|
(*bufferAllocation)--;
|
||||||
|
}
|
||||||
|
// We increment the producerAvailableSlots since there is
|
||||||
|
// now more room to insert values in the buffer.
|
||||||
|
producerAvailableSlots->release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
int
|
||||||
|
main(int argc, char **argv) {
|
||||||
|
// This values are defaults that the user may change, argumentAsNumber function takes care
|
||||||
|
// of this.
|
||||||
|
int numberOfProducers = 5;
|
||||||
|
int numberOfConsumers = 6;
|
||||||
|
|
||||||
|
// First argument is the numberOfProducers, the second is the numberOfConsumers, sadly
|
||||||
|
// C++ won't allow us to have runtime defined semaphore sizes so we cannot have
|
||||||
|
// runtime defined buffer size..
|
||||||
|
argumentAsNumber(argc, argv, 0, std::string("numberOfProducers"), &numberOfProducers);
|
||||||
|
argumentAsNumber(argc, argv, 1, std::string("numberOfConsumers"), &numberOfConsumers);
|
||||||
|
|
||||||
|
// We do not want the threads to go out of scope in the for loop of creation.
|
||||||
|
ListOfThreads producers;
|
||||||
|
ListOfThreads consumers;
|
||||||
|
|
||||||
|
// This variable contains the produced values.
|
||||||
|
Buffer buffer(new int[sizeBuffer]);
|
||||||
|
|
||||||
|
// This variable takes care of the buffer size.
|
||||||
|
BufferAllocation bufferAllocation(new int);
|
||||||
|
*bufferAllocation = 0;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* We need two semaphores, one to control the buffer is still not completly filled up and
|
||||||
|
* other to control the buffer is not empty.
|
||||||
|
*/
|
||||||
|
ProducerConsumerSemaphorePtr producerAvailableSlots(new ProducerConsumerSemaphore(sizeBuffer));
|
||||||
|
ProducerConsumerSemaphorePtr consumerAvailableSlots(new ProducerConsumerSemaphore(0));
|
||||||
|
|
||||||
|
// When we edit the buffer we do not want other threads to access the buffer because of race conditions.
|
||||||
|
MutexPtr bufferMutex(new std::mutex);
|
||||||
|
|
||||||
|
// The creation of the producer threads.
|
||||||
|
for (unsigned long i = 0; i < (unsigned long) numberOfProducers; i++) {
|
||||||
|
producers.push_back(std::jthread([buffer, bufferAllocation, producerAvailableSlots, consumerAvailableSlots, bufferMutex] {
|
||||||
|
producerThread(buffer, bufferAllocation, producerAvailableSlots, consumerAvailableSlots, bufferMutex);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
// The creation of the consumer threads.
|
||||||
|
for (unsigned long i = 0; i < (unsigned long) numberOfConsumers; i++) {
|
||||||
|
consumers.push_back(std::jthread([buffer, bufferAllocation, producerAvailableSlots, consumerAvailableSlots, bufferMutex] {
|
||||||
|
consumerThread(buffer, bufferAllocation, producerAvailableSlots, consumerAvailableSlots, bufferMutex);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
argumentAsNumber(int argc, char **argv, int numberOfArgument, std::string nameOfArgument, int *output) {
|
||||||
|
// arg: 0 has to have argc 2 2 < 2(0+2) pass, 2 < 3 (1+2) not pass.
|
||||||
|
if (argc < numberOfArgument + 2) {
|
||||||
|
std::cerr << "Argument with number " << numberOfArgument << " missing, using default for " << nameOfArgument << std::endl;
|
||||||
|
// Not enough arguments.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
// Argument to number conversion.
|
||||||
|
*output = std::atoi(argv[numberOfArgument + 1]);
|
||||||
|
} catch (std::exception &exception) {
|
||||||
|
// Warning the user, that the argument could not be proccesed.
|
||||||
|
std::cerr << "Unable to convert to number " << numberOfArgument << " as number using default for " << nameOfArgument << std::endl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
0
include/.exists
Normal file
0
include/.exists
Normal file
31
meson.build
Normal file
31
meson.build
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
project('tech.owlcode.consumer-producer', 'cpp')
|
||||||
|
add_global_arguments('-std=c++20', '-D_DEFAULT_SOURCE', language : 'cpp')
|
||||||
|
|
||||||
|
inc = include_directories('include')
|
||||||
|
|
||||||
|
sources = [
|
||||||
|
'src/main.cpp',
|
||||||
|
]
|
||||||
|
|
||||||
|
inc = [
|
||||||
|
'include'
|
||||||
|
]
|
||||||
|
|
||||||
|
link_arguments = [
|
||||||
|
]
|
||||||
|
|
||||||
|
executable('consumer-producer',
|
||||||
|
sources,
|
||||||
|
include_directories : inc,
|
||||||
|
install : true,
|
||||||
|
link_args : link_arguments,
|
||||||
|
)
|
||||||
|
|
||||||
|
doxygen = find_program('doxygen', required : false)
|
||||||
|
|
||||||
|
if doxygen.found()
|
||||||
|
message('Doxygen found')
|
||||||
|
run_target('docs', command : [doxygen, meson.source_root() + '/Doxyfile'])
|
||||||
|
else
|
||||||
|
warning('Documentation disabled without doxygen')
|
||||||
|
endif
|
0
src/.exists
Normal file
0
src/.exists
Normal file
161
src/main.cpp
Normal file
161
src/main.cpp
Normal file
@ -0,0 +1,161 @@
|
|||||||
|
#include <memory>
|
||||||
|
#include <iostream>
|
||||||
|
#include <thread>
|
||||||
|
#include <semaphore>
|
||||||
|
#include <string>
|
||||||
|
#include <thread>
|
||||||
|
#include <list>
|
||||||
|
|
||||||
|
void
|
||||||
|
argumentAsNumber(int argc, char **argv, int numberOfArgument, std::string nameOfArgument, int *output);
|
||||||
|
|
||||||
|
// The buffer size can be changed, the bigger it
|
||||||
|
// is, the less the producer threads would
|
||||||
|
// have to wait for a slot in the event of
|
||||||
|
// consumers being unable to keep up with
|
||||||
|
// the generated values.
|
||||||
|
const int sizeBuffer = 10;
|
||||||
|
|
||||||
|
// Handy typedefs to reduce the line noise.
|
||||||
|
// This is very useful for shared_ptr.
|
||||||
|
typedef std::list<std::jthread> ListOfThreads;
|
||||||
|
typedef std::shared_ptr<int[sizeBuffer]> Buffer;
|
||||||
|
typedef std::shared_ptr<int> BufferAllocation;
|
||||||
|
typedef std::counting_semaphore<sizeBuffer> ProducerConsumerSemaphore;
|
||||||
|
typedef std::shared_ptr<ProducerConsumerSemaphore> ProducerConsumerSemaphorePtr;
|
||||||
|
typedef std::shared_ptr<std::mutex> MutexPtr;
|
||||||
|
|
||||||
|
void
|
||||||
|
producerThread(Buffer buffer, BufferAllocation bufferAllocation,
|
||||||
|
ProducerConsumerSemaphorePtr producerAvailableSlots,
|
||||||
|
ProducerConsumerSemaphorePtr consumerAvailableSlots,
|
||||||
|
MutexPtr bufferMutex) {
|
||||||
|
// We are going to produce in the firsts iterations
|
||||||
|
// values from 0 to 50 and then values from
|
||||||
|
// 10 to 50.
|
||||||
|
int result = 0;
|
||||||
|
while (true) {
|
||||||
|
if (result > 50) {
|
||||||
|
result = 10;
|
||||||
|
}
|
||||||
|
// We reduce the available slots for producers.
|
||||||
|
// If we cant this would wait.
|
||||||
|
producerAvailableSlots->acquire();
|
||||||
|
{
|
||||||
|
// We lock the buffer to avoid other
|
||||||
|
// threads to mangle when we are
|
||||||
|
// still writing.
|
||||||
|
std::lock_guard<std::mutex> lk{*bufferMutex};
|
||||||
|
// We set the last element of the buffer
|
||||||
|
// to be the generated value.
|
||||||
|
buffer[*bufferAllocation] = result;
|
||||||
|
// We increment the buffer size, although I
|
||||||
|
// do not know if this is needed to be commented.
|
||||||
|
(*bufferAllocation)++;
|
||||||
|
}
|
||||||
|
// Now there is one more available slot to be consumed.
|
||||||
|
consumerAvailableSlots->release();
|
||||||
|
result++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
consumerThread(Buffer buffer, BufferAllocation bufferAllocation,
|
||||||
|
ProducerConsumerSemaphorePtr producerAvailableSlots,
|
||||||
|
ProducerConsumerSemaphorePtr consumerAvailableSlots,
|
||||||
|
MutexPtr bufferMutex) {
|
||||||
|
// We keep consuming forever in every consumer thread.
|
||||||
|
while (true) {
|
||||||
|
// This attempt to release the consumerAvailableSlots semaphore.
|
||||||
|
// It will wait if the operation cannot be done because
|
||||||
|
// it is already 0.
|
||||||
|
consumerAvailableSlots->acquire();
|
||||||
|
{
|
||||||
|
// We block the buffer and the variable containing its
|
||||||
|
// size so no other thread attempts to write or read there
|
||||||
|
// while we are doing this.
|
||||||
|
std::lock_guard<std::mutex> lk{*bufferMutex};
|
||||||
|
// We read the first value to create a first in first out.
|
||||||
|
// If we do not do this and the producer threads
|
||||||
|
// are more than the consumers or producing values
|
||||||
|
// way faster than they can be processed we would
|
||||||
|
// end with values that are never proccesed.
|
||||||
|
int result = buffer[0];
|
||||||
|
// We move the buffer -1 position each element
|
||||||
|
// so buffer[1] is now buffer[0]
|
||||||
|
for (int i = 1; i < *bufferAllocation; i++) {
|
||||||
|
buffer[i-1] = buffer[i];
|
||||||
|
}
|
||||||
|
std::cout << "Read " << result << " from the pool." << std::endl;
|
||||||
|
// We decrement the buffer allocated size.
|
||||||
|
(*bufferAllocation)--;
|
||||||
|
}
|
||||||
|
// We increment the producerAvailableSlots since there is
|
||||||
|
// now more room to insert values in the buffer.
|
||||||
|
producerAvailableSlots->release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
int
|
||||||
|
main(int argc, char **argv) {
|
||||||
|
// This values are defaults that the user may change, argumentAsNumber function takes care
|
||||||
|
// of this.
|
||||||
|
int numberOfProducers = 5;
|
||||||
|
int numberOfConsumers = 6;
|
||||||
|
|
||||||
|
// First argument is the numberOfProducers, the second is the numberOfConsumers, sadly
|
||||||
|
// C++ won't allow us to have runtime defined semaphore sizes so we cannot have
|
||||||
|
// runtime defined buffer size..
|
||||||
|
argumentAsNumber(argc, argv, 0, std::string("numberOfProducers"), &numberOfProducers);
|
||||||
|
argumentAsNumber(argc, argv, 1, std::string("numberOfConsumers"), &numberOfConsumers);
|
||||||
|
|
||||||
|
// We do not want the threads to go out of scope in the for loop of creation.
|
||||||
|
ListOfThreads producers;
|
||||||
|
ListOfThreads consumers;
|
||||||
|
|
||||||
|
// This variable contains the produced values.
|
||||||
|
Buffer buffer(new int[sizeBuffer]);
|
||||||
|
|
||||||
|
// This variable takes care of the buffer size.
|
||||||
|
BufferAllocation bufferAllocation(new int);
|
||||||
|
*bufferAllocation = 0;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* We need two semaphores, one to control the buffer is still not completly filled up and
|
||||||
|
* other to control the buffer is not empty.
|
||||||
|
*/
|
||||||
|
ProducerConsumerSemaphorePtr producerAvailableSlots(new ProducerConsumerSemaphore(sizeBuffer));
|
||||||
|
ProducerConsumerSemaphorePtr consumerAvailableSlots(new ProducerConsumerSemaphore(0));
|
||||||
|
|
||||||
|
// When we edit the buffer we do not want other threads to access the buffer because of race conditions.
|
||||||
|
MutexPtr bufferMutex(new std::mutex);
|
||||||
|
|
||||||
|
// The creation of the producer threads.
|
||||||
|
for (unsigned long i = 0; i < (unsigned long) numberOfProducers; i++) {
|
||||||
|
producers.push_back(std::jthread([buffer, bufferAllocation, producerAvailableSlots, consumerAvailableSlots, bufferMutex] {
|
||||||
|
producerThread(buffer, bufferAllocation, producerAvailableSlots, consumerAvailableSlots, bufferMutex);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
// The creation of the consumer threads.
|
||||||
|
for (unsigned long i = 0; i < (unsigned long) numberOfConsumers; i++) {
|
||||||
|
consumers.push_back(std::jthread([buffer, bufferAllocation, producerAvailableSlots, consumerAvailableSlots, bufferMutex] {
|
||||||
|
consumerThread(buffer, bufferAllocation, producerAvailableSlots, consumerAvailableSlots, bufferMutex);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
argumentAsNumber(int argc, char **argv, int numberOfArgument, std::string nameOfArgument, int *output) {
|
||||||
|
// arg: 0 has to have argc 2 2 < 2(0+2) pass, 2 < 3 (1+2) not pass.
|
||||||
|
if (argc < numberOfArgument + 2) {
|
||||||
|
std::cerr << "Argument with number " << numberOfArgument << " missing, using default for " << nameOfArgument << std::endl;
|
||||||
|
// Not enough arguments.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
// Argument to number conversion.
|
||||||
|
*output = std::atoi(argv[numberOfArgument + 1]);
|
||||||
|
} catch (std::exception &exception) {
|
||||||
|
// Warning the user, that the argument could not be proccesed.
|
||||||
|
std::cerr << "Unable to convert to number " << numberOfArgument << " as number using default for " << nameOfArgument << std::endl;
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user