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::thread
corresponding 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::thread
s: suchstd::thread
s have no function to execute, thus corresponding to no underlying thread of execution std::thread
that have been joined: after ajoin
, thestd::thread
object no longer corresponding to the underlying thread of execution which has finished running.std::thread
s that have been detached: adetach
severs the connection between astd::thread
object 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::thread
object 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::thread
objects aren’t copyable, so we accepts onlystd::thread
rvalues- 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::thread
object last in case thestd::thread
depends on other data members. get
is provided to access the underlyingstd::thread
object so that we gain the fullstd::thread
interface for free- A check to make sure the
t
is joinable in destructor is necessary in case that clients usedget
to acquiret
and then did a move fromt
or calledjoin
ordetach
ont
, makingt
unjoinable. - 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 ofjoin
ordetach
, another thread could rendert
unjoinable.1
-
In general, simultaneous member function calls on a single object are safe only if all are to
cont
member functions. ↩︎