t.join();
} else {
t.detach();
}
}
}
std::thread& get() { return t; } // see below
private:
DtorAction action;
std::thread t;
};
I hope this code is largely self-explanatory, but the following points may be helpful:
• The constructor accepts only std::thread
rvalues, because we want to move the passed-in std::thread
into the ThreadRAII
object. (Recall that std::thread
objects aren't copyable.)
• The parameter order in the constructor is designed to be intuitive to callers (specifying the std::thread
first and the destructor action second makes more sense than vice versa), but the member initialization list is designed to match the order of the data members' declarations. That order puts the std::thread
object last. In this class, the order makes no difference, but in general, it's possible for the initialization of one data member to depend on another, and because std::thread
objects may start running a function immediately after they are initialized, it's a good habit to declare them last in a class. That guarantees that at the time they are constructed, all the data members that precede them have already been initialized and can therefore be safely accessed by the asynchronously running thread that corresponds to the std::thread
data member.
• ThreadRAII
offers a get
function to provide access to the underlying std::thread
object. This is analogous to the get
functions offered by the standard smart pointer classes that give access to their underlying raw pointers. Providing get
avoids the need for ThreadRAII
to replicate the full std::thread
interface, and it also means that ThreadRAII
objects can be used in contexts where std::thread
objects are required.
• Before the ThreadRAII
destructor invokes a member function on the std::thread
object t
, it checks to make sure that t
is joinable. This is necessary, because invoking join
or detach
on an unjoinable thread yields undefined behavior. It's possible that a client constructed a std::thread
, created a ThreadRAII
object from it, used get
to acquire access to t
, and then did a move from t
or called join
or detach
on it. Each of those actions would render t
unjoinable.
If you're worried that in this code,
if ( t.joinable()) {
if (action == DtorAction::join) {
t.join();
} else {
t.detach();
}
}
a race exists, because between execution of t.joinable()
and invocation of join
or detach
, another thread could render t
unjoinable, your intuition is commendable, but your fears are unfounded. A std::thread
object can change state from joinable to unjoinable only through a member function call, e.g., join
, detach
, or a move operation. At the time a ThreadRAII
object's destructor is invoked, no other thread should be making member function calls on that object. If there are simultaneous calls, there is certainly a race, but it isn't inside the destructor, it's in the client code that is trying to invoke two member functions (the destructor and something else) on one object at the same time. In general, simultaneous member function calls on a single object are safe only if all are to const
member functions (see Item 16).
Employing ThreadRAII
in our doWork
example would look like this:
bool doWork(std::function filter, // as before
int maxVal = tenMillion) {
std::vector goodVals; // as before
ThreadRAII t( // use RAII object
std::thread([&filter, maxVal, &goodVals] {
for (auto i = 0; i <= maxVal; ++i)
{ if (filter(i)) goodVals.push_back(i); }
}),
ThreadRAII::DtorAction::join// RAII action
);
auto nh = t.get().native_handle();
…
if ( conditionsAreSatisfied() ) {
t. get().join();
performComputation(goodVals) ;
return true;
}
return false;
}
In this case, we've chosen to do a join
on the asynchronously running thread in the ThreadRAII
destructor, because, as we saw earlier, doing a detach
could lead to some truly nightmarish debugging. We also saw earlier that doing a join
could lead to performance anomalies (that, to be frank, could also be unpleasant to debug), but given a choice between undefined behavior (which detach would get us), program termination (which use of a raw std::thread
would yield), or performance anomalies, performance anomalies seems like the best of a bad lot.
Alas, Item 39demonstrates that using ThreadRAII
to perform a join
on std::thread
destruction can sometimes lead not just to a performance anomaly, but to a hung program. The “proper” solution to these kinds of problems would be to communicate to the asynchronously running lambda that we no longer need its work and that it should return early, but there's no support in C++11 for interruptible threads . They can be implemented by hand, but that's a topic beyond the scope of this book. [17] You'll find a nice treatment in Anthony Williams' C++ Concurrency in Action (Manning Publications, 2012), section 9.2.
Item 17explains that because ThreadRAII
declares a destructor, there will be no compiler-generated move operations, but there is no reason ThreadRAII
objects shouldn't be movable. If compilers were to generate these functions, the functions would do the right thing, so explicitly requesting their creation isappropriate:
class ThreadRAII {
public:
enum class DtorAction { join, detach }; // as before
ThreadRAII(std::thread&& t, DtorAction a) // as before
: action(a), t(std::move(t)) {}
~ThreadRAII() {
… // as before
}
ThreadRAII(ThreadRAII&&) = default; // support
ThreadRAII& operator=(ThreadRAII&&) = default;// moving
std::thread& get() { return t; } // as before
private: // as before
DtorAction action;
std::thread t;
};
Things to Remember
• Make std::thread
s unjoinable on all paths.
• join
-on-destruction can lead to difficult-to-debug performance anomalies.
• detach
-on-destruction can lead to difficult-to-debug undefined behavior.
• Declare std::thread
objects last in lists of data members.
Item 38: Be aware of varying thread handle destructor behavior.
Item 37explains that a joinable std::thread
corresponds to an underlying system thread of execution. A future for a non-deferred task (see Item 36) has a similar relationship to a system thread. As such, both std::thread
objects and future objects can be thought of as handles to system threads.
Читать дальше