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::threadrvalues, because we want to move the passed-in std::threadinto the ThreadRAIIobject. (Recall that std::threadobjects aren't copyable.)
• The parameter order in the constructor is designed to be intuitive to callers (specifying the std::threadfirst 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::threadobject 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::threadobjects 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::threaddata member.
• ThreadRAIIoffers a getfunction to provide access to the underlying std::threadobject. This is analogous to the getfunctions offered by the standard smart pointer classes that give access to their underlying raw pointers. Providing getavoids the need for ThreadRAIIto replicate the full std::threadinterface, and it also means that ThreadRAIIobjects can be used in contexts where std::threadobjects are required.
• Before the ThreadRAIIdestructor invokes a member function on the std::threadobject t, it checks to make sure that tis joinable. This is necessary, because invoking joinor detachon an unjoinable thread yields undefined behavior. It's possible that a client constructed a std::thread, created a ThreadRAIIobject from it, used getto acquire access to t, and then did a move from tor called joinor detachon it. Each of those actions would render tunjoinable.
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 joinor detach, another thread could render tunjoinable, your intuition is commendable, but your fears are unfounded. A std::threadobject 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 ThreadRAIIobject'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 constmember functions (see Item 16).
Employing ThreadRAIIin our doWorkexample 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 joinon the asynchronously running thread in the ThreadRAIIdestructor, because, as we saw earlier, doing a detachcould lead to some truly nightmarish debugging. We also saw earlier that doing a joincould 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::threadwould yield), or performance anomalies, performance anomalies seems like the best of a bad lot.
Alas, Item 39demonstrates that using ThreadRAIIto perform a joinon std::threaddestruction 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 ThreadRAIIdeclares a destructor, there will be no compiler-generated move operations, but there is no reason ThreadRAIIobjects 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::threads 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::threadobjects last in lists of data members.
Item 38: Be aware of varying thread handle destructor behavior.
Item 37explains that a joinable std::threadcorresponds 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::threadobjects and future objects can be thought of as handles to system threads.
Читать дальше