Modern Effective C++ - Examining the different thread destruct behaviors

We saw the destruction of std::thread in the previous article and how to handle it properly. Just like std::thread, even std::async / std::future is an handle on the underlying thread. So in this article, we examine std::future and it's exit behavior more closely.

From here on, the called function is referred as callee and the caller function is simply referred as caller. The caller invokes the callee using std::async. We can visualize the return value of std::async i.e std::future as a channel between caller and callee. The callee computes the results and writes the result into it's end of the channel. The caller can then read it from it's end.

A question then arises about the exact location to store the result safely. We cannot store the result with the callee because the result might be computed by the callee( and might have exited) much before get() is called on the future. The result cannot be stored in the caller either because std::future can be used created std::shared_future.

Since we cannot store the result with the caller or the callee, we store the result in a location outside both these functions. This location is situated in heap, and is usually referred to as shared state. Now we can visualize this shared_state as a intermediary between caller and the callee.

Here are two important qualities of std::future.

  1. The last std::future referring to the shared_state, for a task which is not deferred, is blocked till the task finishes. This rule refers to the shared state created by std::async only. More about this at the end.
  2. For deferred tasks, the std::future is simply destroyed when it goes out of scope. The behavior is similar to a thread  calling an implicit detach.

Both the cases are demonstrated with examples -




The only difference between the two programs is in line 14.

Recall that when std::async is launched without the launch policy or with the default launch policy, the program is free to run it either asynchronously or synchronously. It takes this decision at runtime depending on the load on the system. In this situation, the future's exit behavior also becomes undeterministic.

There is also another multi-threading API from which we can get the std::future. It is std::packaged_task. But it's behavior is not the same as we have been discussing so far because the shared state is not created by std::async. Have a look at the program below -



The packaged task has been moved to the thread, and the thread is detached. But the get API of the std::future forces the program to wait till the detached thread has finished it's execution. If we comment out line 18, we get the same output, but the program exits with a runtime error because the thread t is in unjoinable state when it exits.

Instead of creating a std::thread from std::packaged_task, we can pass the packaged_task to std::async. But there is no point in doing so, since async does everything that packaged_task does.

This article is inspired by Item 38 of Effective Modern C++ by Scott Meyers. Please check out the book for more in-depth explanation.


Modern Effective C++ - Examining the different thread destruct behaviors Modern Effective C++ - Examining the different thread destruct behaviors Reviewed by zeroingTheDot on May 21, 2018 Rating: 5

No comments:

Powered by Blogger.