In the header file
workerpool.hpp, we declare the WorkerPool class.
The argument of the constructor is the number of workers. If the number of
workers is zero, all tasks are executed serially (which can be convenient for
debugging). A WorkerPool can not be copied, so we disable the copy
constructor and copy assignment constructor. A Task is defined as a
function that takes no arguments and returns nothing. A Task can be
passed to the worker pool using the push_back() method (which name
is inspired by std::list::push_back()). The final public method is
the sync() method that waits for all tasks to be completed.
The private methods and members of
WorkerPool are a vector of
worker threads, a list of (unfinished) tasks, the worker routine, and some
synchronization objects. The boolean abort_flag is used to get the
workers to exit their worker routines. The count integer represents
the number of unfinished tasks. Tasks are guarded by the
task_mut mutex, and count by the
count_mut mutex. The task_cv condition variable is
used to signal that a new Task is added to the list of tasks, while
the count_cv is used to signal that another task has been
completed.
The source file contains the definitions of the methods of
WorkerPool. The constructor initiates a number of worker threads
and starts their worker_routine(). The descructor makes sure that
all threads exit their worker routine and joins the threads. This means that the
WorkerPool uses the RAII technique. The
push_back() method evaluates the Task object if there
are no workers, and otherwise adds the task to the tasks list,
after increasing the count. We signal that a task has been added in
case a worker is sleeping. The sync() method only returns when
count == 0. Finally, the most important part of the code is the
worker_routine(). When the list tasks is empty, a
worker waits until a signal is given that a new tasks is added, or the
abort_flag is set. In the first case, the task is executed, in the
second case, the worker exits the worker_routine().
Example: drawing the Mandelbrot set in parallel
To give a nice example of how the
WorkerPool should be used, I
modified some code from
Farouk Ounane
for drawing the Mandelbrot set in C++. Each Task determines if a
point in the complex plane is part of the Mandelbrot set or not. We use a
closure (lambda expression) to define these tasks, and pass them to the
WorkerPool. The result is shown above. To compile the C++ program with
gcc, put the files workerpool.cpp,
workerpool.hpp and mandelbrot.cpp in the same
directory, and execute from the terminal
g++ workerpool.cpp mandelbrot.cpp -o mandelbrot -pthread -std=c++11
