This is a slightly-revised version of my original post. It reflects information I've since received that confirms some of the suppositions I'd been making, and it rewords some things to clarify them.
It's comparatively well known that the std::future returned from std::async will block in its destructor until the asynchronously running thread has completed:
void f()
{
std::future<void> fut = std::async(std::launch::async,
[] { /* compute, compute, compute */ });
} // block here until thread spawned by
// std::async completes
Only std::futures returned from std::async behave this way, so I had been under the impression that they were special. But now I believe otherwise. I now believe that
all futures must behave the same way, regardless of whether they originated in std::async. This does
not mean that all futures must block in their destructors. The story is more nuanced than that.
There's definiately something special about std::async, because futures you get from other sources (e.g., from a std::promise or a std:: packaged_task) don't block in their destructors. But how does the specialness of std::async affect the behavior of futures?
C++11 futures are the caller's end of a communications channel that begins with a callee that's (typically) called asynchronously. When the called function has a result to communicate to its caller, it performs a
set operation on the std::promise corresponding to the future. That is, an asynchronous callee
sets a promise (i.e., writes a result to the communication channel between it and its caller), and its caller
gets the future (i.e., reads the result from the communications channel).
(As usual, I'm ignoring a host of details that don't affect the basic story I'm telling. Such details including return values versus exceptions,
waiting versus
getting, unshared versus shared futures, etc.)
Between the time a callee sets its promise and its caller does a corresponding get, an arbitrarily long time may elapse. (In fact, the get may never take place, but that's a detail I'm ignoring.) As a result, the std::promise object that was set may be destroyed before a get takes place. This means that the value with which the callee sets the promise can't be stored in the promise--the promise may not have a long enough lifetime. The value also can't be stored in the future corresponding to the promise, because the std::future returned from std::async could be moved into a std::shared_future before being destroyed, and the std::shared_future could then be copied many times to new objects, some of which would subsequently be destroyed. In that case, which future would hold the value returned by the callee?
Because neither the promise nor the future ends of the communications channel between caller and callee are suitable for storing the result of an asynchronously invoked function, it's stored in a neutral location. This location is known as the
shared state. There's nothing in the C++ standard library corresponding to the shared state. No class, no type, no function. In practice, I'm guessing it's implemented as a class that's templatized on at least the type of the result to be communicated between callee and caller.
The special behavior commonly attributed to futures returned by std::async is actually determined by the shared state. Once you know what to look for, this is indicated in only moderately opqaque prose (for the standard) in 30.6.8/3, where we learn that
The thread object [for the function to be run asynchronously] is stored in the shared state and affects the behavior of any asynchronous return objects [e.g., futures] that reference that state.
and in 30.6.8/5, where we read:
the thread completion [for the function run asynchronously] synchronizes with [i.e., occurs before] [1] the return from the first function that successfully detects the ready status of the shared state or [2] with the return from the last function that releases the shared state, whichever happens first.
It's provision [2] that's relevant to us here. It tells us that if a future holds the last reference to the shared state corresponding to a call to std::async, that future's destructor must block until the thread for the asynchronously running function finishes. This is a requirement for
any future object. There is nothing special about std::futures returned from std::async. Rather, the specialness of std::async is manifested in its shared state.
By the way, when I write that the "future's destructor must block," I don't mean it literally. The standard just says that the function releasing the last reference to a shared state corresponding to a std::async call can't return as long as the thread for the asynchronously running function is still executing. That behavior doesn't have to be implemented by having a future's destructor directly block. The future destructor might simply call a member function to decrement the reference count on the shared state. Inside that call, if the result of the decrement was zero and the shared state corresponded to a std::async call, the member function would simply wait until the thread running the asynchronously running function completed before it returned to the future destructor. From the future's point of view, it merely made a synchronous call to a function to decrement the reference count on the shared state. The runtime behavior, however, would be that it could block until the asynchronously running thread completed.
The provision stating that, essentially, the shared state corresponding to a call to std::async must somehow indicate that the last future referring to them must block until the associated thread has finished running, is not popular. It's been proposed to be changed, and some standard library implementations (e.g., Microsoft's) have already revised their implementations to eliminate the "futures from std::async block in their destructors" behavior. That makes it trickier for you to test the behavior of this part of the standard, because the library you use may be deliberately nonconformant in this area.
Scott
PS - The reason I got caught up in this matter was that I was trying to find a way to perform the moral equivalent of a detach on a thread spawned via std::async. Because I believed it was the std::future returned from std::async that was special, I started experimenting with things like moving that std::future into a std::shared_future in an attempt to return from the function calling std::async before the asynchronously running function had finished. But since it's the shared state that's special, not the std::future, this approach seems doomed. If you know how to get detach-like behavior when using std::async (without the cooperation of the function being run asynchronously), please let me know!