Our preference would be to employ a task-based design for this (see Item 35), but let's assume we'd like to set the priority of the thread doing the filtering. Item 35explains that that requires use of the thread's native handle, and that's accessible only through the std::thread
API; the task-based API (i.e., futures) doesn't provide it. Our approach will therefore be based on threads, not tasks.
We could come up with code like this:
constexpr auto tenMillion = 10000000; // see Item 15
// for constexpr
bool doWork(std::function filter, // returns whether
int maxVal = tenMillion) // computation was
{ // performed; see
// Item 2 for
// std::function
std::vector goodVals; // values that
// satisfy filter
std::thread t([&filter, maxVal, &goodVals] // populate
{ // goodVals
for (auto i = 0; i <= maxVal; ++i)
{ if (filter(i)) goodVals.push_back(i); }
});
auto nh = t.native_handle(); // use t's native
… // handle to set
// t's priority
if ( conditionsAreSatisfied() ) {
t.join(); // let t finish
performComputation(goodVals) ;
return true; // computation was
} // performed
return false; // computation was
} // not performed
Before I explain why this code is problematic, I'll remark that tenMillion
's initializing value can be made more readable in C++14 by taking advantage of C++14's ability to use an apostrophe as a digit separator:
constexpr auto tenMillion = 10 '000 '000; // C++14
I'll also remark that setting t
's priority after it has started running is a bit like closing the proverbial barn door after the equally proverbial horse has bolted. A better design would be to start t
in a suspended state (thus making it possible to adjust its priority before it does any computation), but I don't want to distract you with that code. If you're more distracted by the code's absence, turn to Item 39, because it shows how to start threads suspended.
But back to doWork
. If conditionsAreSatisfied()
returns true
, all is well, but if it returns false
or throws an exception, the std::thread
object t
will be joinable when its destructor is called at the end of doWork
. That would cause program execution to be terminated.
You might wonder why the std::thread
destructor behaves this way. It's because the two other obvious options are arguably worse. They are:
• An implicit join
. In this case, a std::thread
's destructor would wait for its underlying asynchronous thread of execution to complete. That sounds reasonable, but it could lead to performance anomalies that would be difficult to track down. For example, it would be counterintuitive that doWork
would wait for its filter to be applied to all values if conditionsAreSatisfied()
had already returned false
.
• An implicit detach
. In this case, a std::thread
's destructor would sever the connection between the std::thread
object and its underlying thread of execution. The underlying thread would continue to run. This sounds no less reasonable than the join
approach, but the debugging problems it can lead to are worse. In doWork
, for example, goodVals
is a local variable that is captured by reference. It's also modified inside the lambda (via the call to push_back
). Suppose, then, that while the lambda is running asynchronously, conditionsAreSatisfied()
returns false
. In that case, doWork
would return, and its local variables (including goodVals
) would be destroyed. Its stack frame would be popped, and execution of its thread would continue at doWork
's call site.
Statements following that call site would, at some point, make additional function calls, and at least one such call would probably end up using some or all of the memory that had once been occupied by the doWork
stack frame. Let's call such a function f
. While f
was running, the lambda that doWork
initiated would still be running asynchronously. That lambda could call push_back
on the stack memory that used to be goodVals
but that is now somewhere inside f
's stack frame. Such a call would modify the memory that used to be goodVals
, and that means that from f
's perspective, the content of memory in its stack frame could spontaneously change! Imagine the fun you'd have debugging that .
The Standardization Committee decided that the consequences of destroying a joinable thread were sufficiently dire that they essentially banned it (by specifying that destruction of a joinable thread causes program termination).
This puts the onus on you to ensure that if you use a std::thread
object, it's made unjoinable on every path out of the scope in which it's defined. But covering every path can be complicated. It includes flowing off the end of the scope as well as jumping out via a return
, continue
, break
, goto
or exception. That can be a lot of paths.
Any time you want to perform some action along every path out of a block, the normal approach is to put that action in the destructor of a local object. Such objects are known as RAII objects , and the classes they come from are known as RAII classes . ( RAII itself stands for “Resource Acquisition Is Initialization,” although the crux of the technique is destruction, not initialization). RAII classes are common in the Standard Library. Examples include the STL containers (each container's destructor destroys the container's contents and releases its memory), the standard smart pointers (Items 18–20explain that std::unique_ptr
's destructor invokes its deleter on the object it points to, and the destructors in std::shared_ptr
and std::weak_ptr
decrement reference counts), std::fstream
objects (their destructors close the files they correspond to), and many more. And yet there is no standard RAII class for std::thread
objects, perhaps because the Standardization Committee, having rejected both join
and detach
as default options, simply didn't know what such a class should do.
Fortunately, it's not difficult to write one yourself. For example, the following class allows callers to specify whether join
or detach
should be called when a ThreadRAII
object (an RAII object for a std::thread
) is destroyed:
class ThreadRAII {
public:
enum class DtorAction { join, detach }; // see Item 10 for
// enum class info
ThreadRAII(std::thread&& t, DtorAction a) // in dtor, take
: action(a), t(std::move(t)) {} // action a on t
~ThreadRAII()
{ // see below for
if (t.joinable()) { // joinability test
if (action == DtorAction::join) {
Читать дальше