This kind of bug is easy to overlook during development and unit testing, because it may manifest itself only under heavy loads. Those are the conditions that push the machine towards oversubscription or thread exhaustion, and that's when a task may be most likely to be deferred. After all, if the hardware isn't threatened by oversubscription or thread exhaustion, there's no reason for the runtime system not to schedule the task for concurrent execution.
The fix is simple: just check the future corresponding to the std::async
call to see whether the task is deferred, and, if so, avoid entering the timeout-based loop. Unfortunately, there's no direct way to ask a future whether its task is deferred. Instead, you have to call a timeout-based function — a function such as wait_for
. In this case, you don't really want to wait for anything, you just want to see if the return value is std::future_status::deferred
, so stifle your mild disbelief at the necessary circumlocution and call wait_for
with a zero timeout:
auto fut = std::async(f); // as above
if (fut.wait_for(0s) == // if task is
std::future_status::deferred) // deferred...
{
// ...use wait or get on fut
… // to call f synchronously
} else {// task isn't deferred
while (fut.wait_for(100ms) != // infinite loop not
std::future_status::ready) { // possible (assuming
// f finishes)
… // task is neither deferred nor ready,
// so do concurrent work until it's ready
}
… // fut is ready
}
The upshot of these various considerations is that using std::async
with the default launch policy for a task is fine as long as the following conditions are fulfilled:
• The task need not run concurrently with the thread calling get
or wait
.
• It doesn't matter which thread's thread_local
variables are read or written.
• Either there's a guarantee that get
or wait
will be called on the future returned by std::async
or it's acceptable that the task may never execute.
• Code using wait_for
or wait_until
takes the possibility of deferred status into account.
If any of these conditions fails to hold, you probably want to guarantee that std::async
will schedule the task for truly asynchronous execution. The way to do that is to pass std::launch::async
as the first argument when you make the call:
auto fut = std::async( std::launch::async, f); // launch f
// asynchronously
In fact, having a function that acts like std::async
, but that automatically uses std::launch::async
as the launch policy, is a convenient tool to have around, so it's nice that it's easy to write. Here's the C++11 version:
template
inline
std::future::type>
reallyAsync(F&& f, Ts&&... params) // return future
{ // for asynchronous
return std::async( std::launch::async, // call to f(params...)
std::forward(f),
std::forward(params)...);
}
This function receives a callable object f
and zero or more parameters params
and perfect-forwards them (see Item 25) to std::async
, passing std::launch::async
as the launch policy. Like std::async
, it returns a std::future
for the result of invoking f
on params
. Determining the type of that result is easy, because the type trait std::result_of
gives it to you. (See Item 9for general information on type traits.)
reallyAsync
is used just like std::async
:
auto fut = reallyAsync(f); // run f asynchronously;
// throw if std::async
// would throw
In C++14, the ability to deduce reallyAsync
's return type streamlines the function declaration:
template
inline
auto // C++14
reallyAsync(F&& f, Ts&&... params) {
return std::async(std::launch::async,
std::forward(f),
std::forward(params)...);
}
This version makes it crystal clear that reallyAsync
does nothing but invoke std::async
with the std::launch::async
launch policy.
Things to Remember
• The default launch policy for std::async
permits both asynchronous and synchronous task execution.
• This flexibility leads to uncertainty when accessing thread_local
s, implies that the task may never execute, and affects program logic for timeout-based wait calls.
• Specify std::launch::async
if asynchronous task execution is essential.
Item 37: Make std::thread
s unjoinable on all paths.
Every std::thread
object is in one of two states: joinable or unjoinable . A joinable std::thread
corresponds to an underlying asynchronous thread of execution that is or could be running. A std::thread
corresponding to an underlying thread that's blocked or waiting to be scheduled is joinable, for example. std::thread
objects corresponding to underlying threads that have run to completion are also considered joinable.
An unjoinable std::thread
is what you'd expect: a std:: thread
that's not joinable. Unjoinable std::thread
objects include:
• Default-constructed std::thread
s. Such std::thread
s have no function to execute, hence don't correspond to an underlying thread of execution.
• std::thread
objects that have been moved from. The result of a move is that the underlying thread of execution a std::thread
used to correspond to (if any) now corresponds to a different std::thread
.
• std::thread
s that have been join
ed. After a join
, the std::thread
object no longer corresponds to the underlying thread of execution that has finished running.
• std::thread
s that have been detach
ed. A detach
severs the connection between a std::thread
object and the underlying thread of execution it corresponds to.
One reason a std::thread
's joinability is important is that if the destructor for a joinable thread is invoked, execution of the program is terminated. For example, suppose we have a function doWork
that takes a filtering function, filter
, and a maximum value, maxVal
, as parameters. doWork
checks to make sure that all conditions necessary for its computation are satisfied, then performs the computation with all the values between 0 and maxVal
that pass the filter. If it's time-consuming to do the filtering and it's also time-consuming to determine whether doWork
's conditions are satisfied, it would be reasonable to do those two things concurrently.
Читать дальше