From this perspective, it's interesting that std::thread
s and futures have such different behaviors in their destructors. As noted in Item 37, destruction of a joinable std::thread
terminates your program, because the two obvious alternatives — an implicit join
and an implicit detach
— were considered worse choices. Yet the destructor for a future sometimes behaves as if it did an implicit join
, sometimes as if it did an implicit detach
, and sometimes neither. It never causes program termination. This thread handle behavioral bouillabaisse deserves closer examination.
We'll begin with the observation that a future is one end of a communications channel through which a callee transmits a result to a caller. [18] Item 39 explains that the kind of communications channel associated with a future can be employed for other purposes. For this Item, however, we'll consider only its use as a mechanism for a callee to convey its result to a caller.
The callee (usually running asynchronously) writes the result of its computation into the communications channel (typically via a std::promise
object), and the caller reads that result using a future. You can think of it as follows, where the dashed arrow shows the flow of information from callee to caller:
But where is the callee's result stored? The callee could finish before the caller invokes get on a corresponding future, so the result can't be stored in the callee's std::promise
. That object, being local to the callee, would be destroyed when the callee finished.
The result can't be stored in the caller's future, either, because (among other reasons) a std::future
may be used to create a std::shared_future
(thus transferring ownership of the callee's result from the std::future
to the std::shared_future
), which may then be copied many times after the original std::future
is destroyed. Given that not all result types can be copied (i.e., move-only types) and that the result must live at least as long as the last future referring to it, which of the potentially many futures corresponding to the callee should be the one to contain its result?
Because neither objects associated with the callee nor objects associated with the caller are suitable places to store the callee's result, it's stored in a location outside both. This location is known as the shared state . The shared state is typically represented by a heap-based object, but its type, interface, and implementation are not specified by the Standard. Standard Library authors are free to implement shared states in any way they like.
We can envision the relationship among the callee, the caller, and the shared state as follows, where dashed arrows once again represent the flow of information:
The existence of the shared state is important, because the behavior of a future's destructor — the topic of this Item — is determined by the shared state associated with the future. In particular,
• The destructor for the last future referring to a shared state for a non-deferred task launched via std::async
blocksuntil the task completes. In essence, the destructor for such a future does an implicit join on the thread on which the asynchronously executing task is running.
• The destructor for all other futures simply destroys the future object. For asynchronously running tasks, this is akin to an implicit detach
on the underlying thread. For deferred tasks for which this is the final future, it means that the deferred task will never run.
These rules sound more complicated than they are. What we're really dealing with is a simple “normal” behavior and one lone exception to it. The normal behavior is that a future's destructor destroys the future object. That's it. It doesn't join
with anything, it doesn't detach
from anything, it doesn't run anything. It just destroys the future's data members. (Well, actually, it does one more thing. It decrements the reference count inside the shared state that's manipulated by both the futures referring to it and the callee's std::promise
. This reference count makes it possible for the library to know when the shared state can be destroyed. For general information about reference counting, see Item 19.)
The exception to this normal behavior arises only for a future for which all of the following apply:
• It refers to a shared state that was created due to a call to std::async
.
• The task's launch policy is std::launch::async
(see Item 36), either because that was chosen by the runtime system or because it was specified in the call to std::async
.
• The future is the last future referring to the shared state. For std::future
s, this will always be the case. For std::shared_future
s, if other std::shared_future
s refer to the same shared state as the future being destroyed, the future being destroyed follows the normal behavior (i.e., it simply destroys its data members).
Only when all of these conditions are fulfilled does a future's destructor exhibit special behavior, and that behavior is to block until the asynchronously running task completes. Practically speaking, this amounts to an implicit join
with the thread running the std::async
-created task.
It's common to hear this exception to normal future destructor behavior summarized as “Futures from std::async
block in their destructors.” To a first approximation, that's correct, but sometimes you need more than a first approximation. Now you know the truth in all its glory and wonder.
Your wonder may take a different form. It may be of the “I wonder why there's a special rule for shared states for non-deferred tasks that are launched by std::async
” variety. It's a reasonable question. From what I can tell, the Standardization Committee wanted to avoid the problems associated with an implicit detach
(see Item 37), but they didn't want to adopt as radical a policy as mandatory program termination (as they did for joinable std::thread
s — again, see Item 37), so they compromised on an implicit join
. The decision was not without controversy, and there was serious talk about abandoning this behavior for C++14. In the end, no change was made, so the behavior of destructors for futures is consistent in C++11 and C++14.
The API for futures offers no way to determine whether a future refers to a shared state arising from a call to std::async
, so given an arbitrary future object, it's not possible to know whether it will block in its destructor waiting for an asynchronously running task to finish. This has some interesting implications:
// this container might block in its dtor, because one or more
// contained futures could refer to a shared state for a non-
// deferred task launched via std::async
std::vector > futs; // see Item 39 for info
// on std::future
class Widget { // Widget objects might
Читать дальше