Join-on-destruction can lead to difficult-to-debug performance anomalies; while detach-on-destruction can lead to difficult-to-debug undefined behavior.
Every std::thread object is in one of two states:
- joinable: corresponding to an underlying asynchronous thread of execution that is or could be running.
- A
std::threadcorresponding to an underlying thread that’s waiting to be scheduled, blocked, or have run to comletion are all considered joinable.
- A
- unjoinable:
- Default-constructed
std::threads: suchstd::threads have no function to execute, thus corresponding to no underlying thread of execution std::threadthat have been joined: after ajoin, thestd::threadobject no longer corresponding to the underlying thread of execution which has finished running.std::threads that have been detached: adetachsevers the connection between astd::threadobject and the underlying thread of execution it corresponds to.
- Default-constructed
Due to the requirement that if the destructor for a joinable thread is invoked, execution of the program (i.e., all threads) is terminated. The destructor of std::thread behaves in this way because the two other obvious options are arguably worse:
- An implicit
join: thus astd::thread’s destructor would wait for its underlying asynchronous thread of execution to complete, leading to performance anomalies that would be difficult to track down. - An implicit
detach: then astd::thread’s destructor would sever the connection between thestd::threadobject and its underlying thread of execution, which would continue to run. That is wild.
The Standardization Committee decided this program termination behavior just to tell us that we need to ensure a std::thread object is made unjoinable on every path out of the scope in which it’s defined. That is, we can use RAII technique to take care of that.
|
|
A few points:
std::threadobjects aren’t copyable, so we accepts onlystd::threadrvalues- The parameter order in the constructor is designed to be intuitive to callers, but the member initialization list is designed to match the order of the data members’ declarations, in which we put the
std::threadobject last in case thestd::threaddepends on other data members. getis provided to access the underlyingstd::threadobject so that we gain the fullstd::threadinterface for free- A check to make sure the
tis joinable in destructor is necessary in case that clients usedgetto acquiretand then did a move fromtor calledjoinordetachont, makingtunjoinable. - If in the client code there are simultaneous calls trying to invoke two member functions (the destructor and something else) on one object at the same time, there is a race: between execution of
t.joinable()and invocation ofjoinordetach, another thread could rendertunjoinable.1
-
In general, simultaneous member function calls on a single object are safe only if all are to
contmember functions. ↩︎