Packaged task with a restaurant
The std::packaged_task
is one of the possible ways of
associating a task with a std::future
. The benefit of the
packaged task is to decouple the creation of the future with the execution of
the task.
In the following, we will look how to exploit this decoupling.
Restaurant
Imagine a restaurant. There are a waiter and a cook. The procedure usually goes like this
- a customer orders a meal,
- the waiter records the order and passes it to the cook,
- the cook receives the order, cooks the meal and sends it back to the waiter,
- the waiter serves the meal.
In our particular example, we have a moody cook. He worked in a fast food restaurant before and he doesn’t want to make hamburgers any more. Unfortunately, our waiter is not aware of this issue.
We would like to model the restaurant with our code. The cook and the waiter
will be represented as two threads. The tasks
(std::packaged_task
) will be orders. The waiter will create
the task, hold its std::future
and push the task to the cook.
The cook will execute the task: make a meal out of the order.
Since we now know the general idea of the program, let’s look at the code.
The code begins with include statements. We declare global mutex, queue and condition variable. This variables will be used for passing the tasks between the threads. You should review the Passing data with condition variable if you are not comfortable passing data (in our case the package tasks) with condition variable.
The boolean variable CLOSED
marks whether the restaurant is
open or closed.
The make_break
function is familiar to us. It was a building
block of many previous examples. We use it to make the output nicer.
Next follows the code for the cook.
The cooking()
function loops until the restaurant closes. It
gets a cooking order from the queue with the help of the condition variable.
Passing data with condition
variable explains how this
works. Then, the function executes the cooking order – the cook prepares the
meal.
The function cook_the_meal(...)
also models the cook. Here, the
moody behavior of the cook is defined. If the meal is hamburger, the cook
doesn’t want to prepare it, therefore the function returns
false
. Otherwise, the cook prepares the meal and the
function returns true
.
Now follows the code which describes the waiter.
Let’s explain it from the bottom up.
serve_orders
There are three orders: steak, hamburger and cheesecake. The waiter first adds
all three orders to the queue by calling the add_order
function. The function returns the future which represents the boolean –
true
if the meal is cooked or false
if
something went wrong.
Later, the waiter serves the meals via serve
function. The
std::future
is not copyable, therefore we need to use
std::move
when passing the future as an argument.
serve
The waiter checks the results of the cooking by calling the
.get()
member function of the future. If it returns
true
, everything went well and the waiter serves the meal.
Otherwise, the waiter apologies to the customer and kindly asks her/him to order
again.
add_order
The add_order
function passes the orders between the threads
using the same technique as in Passing data with condition
variable.
It creates the package task from the string which represents the meal.
The std::bind
binds the function
cook_the_meal
with the std::string
argument. If we have
then invoking make_cake()
is the same as
The advantage of using std::bind
is that we
pass only the std::packaged_task< bool() >
to the other
thread instead of passing std::packaged_task< bool(std::string)
>
together with all the arguments for the packaged tasks.
After the creation of packaged task, we get the future from the packaged task,
push the packaged task to the queue and notify the condition variable. (This
technique is the same as in Passing data with condition
variable.) Note that
std::packaged_task
is not copyable, therefore
std::move
moves it to the queue. At the end, the function
returns the future.
We decoupled the creation of the future with the execution of the task. The
future is created in add_order
. The task is executed
in cooking
, which will run in a different thread than
add_order
.
The main function creates two threads: cook
and
waiter
with appropriate functions. After some time, we close
the restaurant and the main function ends.
The entire source code is available here.
Running the restaurant
This is one possible output of the program.
The picture below describes the flow of the program.
At the beginning, the waiter sends three orders to the cook. The cook picks up
the orders and starts making the meals. When the cook finishes the meal
(successfully or unsuccessfully), he informs the waiter about it (via
std::future
). Once the waiter gets the prepared meal, he serves
it.
The figure nicely describes the decoupling. The waiter’s thread is responsible for the creation of the packaged tasks and the cook’s thread is responsible for the execution of the tasks.
Summary
The std::packaged_task
decouples the creation of the future
with the execution of the task. We learned how to benefit from this
decoupling. We created tasks in one thread and we executed them in the other
thread.
Links: