Monday, March 25, 2013

Thread Handle Destruction and Behavioral Consistency

Suppose you fire up a thread in a function, then return from the function without joining or detaching the thread:
void doSomeWork();

void f1()
{
  std::thread t(doSomeWork);
  ...                          // no join, no detach
}
What happens?

Your program is terminated. The destructor of a std::thread object that refers to a "joinable" thread calls std::terminate.

Now suppose you do the same thing, except instead of firing up the thread directly, you do it via std::async:
void f2()
{
  auto fut = std::async(std::launch::async, doSomeWork);
  ...                          // no get, no wait
}
Now what happens?

Your function blocks until the asychronously running thread completes. This is because the shared state for a std::async call causes the last future referring to that shared state to block in its destructor. Practically speaking, the destructor for the final future referring to a std::async shared state does an implicit join on the asynchronously running thread.

(The behavior I'm describing is mandated by the standard. Some implementations, notably Microsoft's, don't behave this way, because the standardization committee is considering changing this aspect of the standard, and Microsoft has implemented the revised behavior they believe will ultimately be adopted.)

Finally, suppose you create a packaged_task for the function to be run asynchronously, then you detach from the thread running the packaged_task, while retaining the future for the packaged_task:
void f3()
{
  std::packaged_task<void()> pt(doSomeWork);
  auto fut = pt.get_future();
  std::thread(std::move(pt)).detach();
  ...                          // no get, no wait
}
Now what happens?

Your function returns, even if the function to be run asynchronously is still running. In essence, the thread is detached. The destructor of the thread object no longer refers to a joinable thread (thanks to the call to detach), so it doesn't call std::terminate, and the destructor of the std::future refer doesn't refer to a shared state for a call to std::async, so it doesn't performs an implicit join.

"So what's your point?," you may be wondering. Well, we can think of both std::thread objects and futures as handles for asynchronously running threads, and it's interesting to note that when such handles are destroyed, in some cases, we terminate, in others we do an implicit join, and in others we do an implicit detach. As I've been known to put it, the standardaization committee, when faced with a choice of three possible behaviors, chose all three.

In fact, I'm making this post at the request of a member of the standardization committee who thought it would be worthwhile to point out this inconsistency in the standard's treatment of thread handles. Whether anything will be done about it remains to be seen. If the specification for std::async is modified such that its shared state no longer causes the blocking behavior I described in my last post, that would eliminate the implicit join behavior, but I'm not convinced that such a change is a shoe-in for adoption. The problem is that such a change to the standard could silently break the behavior of existing programs (i.e., code that depends on the implicit join in future destructors that are the final reference to a shared state coming from std::async), and the standardization committee is generally very reluctant to adopt changes that can silently change the behavior of conforming programs.

Scott




No comments: