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