This presentation is about multitasking with std::future.
Presentation by Dmytro Gurin (Lead Software Engineer, GlobalLogic, Kyiv), delivered at GlobalLogic C++ TechTalk in Lviv, September 18, 2014.
More details -
http://www.globallogic.com.ua/press-releases/lviv-cpp-techtalk-coverage
3. [C++03, 1.9.6]
• The observable behavior of the abstract machine is
its sequence of reads and writes to volatile data and
calls to library I/O functions.
[C++11, 1.10.1]:
• A thread of execution (also known as a thread) is a
single flow of control within a program... The
execution of the entire program consists of an
execution of all of its threads...
4. [C++11, 30.1.1]:
• The following subclauses describe components
to create and manage threads (1.10), perform
mutual exclusion, and communicate
conditions and values between threads…
Subclause Header
Threads <thread>
Mutual exclusion <mutex>
Condition variables <condition_variable>
Futures <future>
5. Having independent time consuming tasks:
// ...
device target_device;
target_device.initialize(); // <- time consuming
configuration_storage db;
configuration config;
db.load(config); // <- time consuming
target_device.configure(config);
6. Execute time consuming tasks
simultaneously, naive approach:
// ...
configuration_storage db;
configuration config;
thread loader([&db, &config]{ db.load(config); });
// <- run loading config in a separate thread
device target_device;
target_device.initialize();
// <- continue device initialization in current thread
loader.join();
// <- assume loading done at this point
target_device.configure(config);
7. Execute time consuming tasks
simultaneously, thread pool approach:
class async_exec_service {
public:
void exec(function<void()> f);
};
// ...
async_exec_service exec_service;
// ...
configuration_storage db;
configuration config;
exec_service.exec([&db, &config]{ db.load(config); });
// <- run loading config in the thread pool
device target_device;
target_device.initialize();
// <- continue device initialization in current thread
// hmmm... are we ready for proceed ???
target_device.configure(config);
8. Execute time consuming tasks simultaneously, thread
pool approach with synchronization:
class async_exec_service { public: void exec(function<void()> f); };
// ...
async_exec_service exec_service;
// ...
configuration_storage db;
unique_ptr<configuration> config;
mutex config_guard;
condition_variable config_cv;
exec_service.exec([&]{
unique_lock<mutex> lock(config_guard);
config.reset(new configuration);
db.load(*config);
config_cv.notify_one();
}); // <- run loading config in the thread pool
device target_device;
target_device.initialize(); // continue device initialization in current thread
{ // wait for configuration loading done
unique_lock<mutex> lock(config_guard);
config_cv.wait(lock, [&]{ return (config != nullptr); });
}
// we are ready for proceed
target_device.configure(*config);
9. The Standard Library provides a unified
solution communicate data between
threads:
• shared state object – privately held object with a
placeholder for result and some auxiliary data;
• asynchronous return object – reads result from
a shared state;
• asynchronous provider - provides shared state
object with a value.
11. Execute time consuming tasks simultaneously, using
future<T>:
class async_exec_service {
public:
template <typename _Function>
auto exec(_Function&& f) -> future<decltype(f())>
{ /*...*/ }
};
async_exec_service exec_service;
// ...
configuration_storage db;
future<configuration> config = exec_service.exec([&db]{
configuration config;
db.load(config);
return config;
});
device target_device;
target_device.initialize();
target_device.configure(config.get());
// wait for result and proceed
12. valid() – tests if the future has a shared
state;
get() – retrieves value, wait if needed;
wait()/wait_for(delay)/wait_until(time) –
waits the future to be populated with result;
share() – makes shared_future from this
future; invalidates this future object.
13. shares its shared state object between
several asynchronous return objects:
the main purpose – signal the result is
ready to multiple waiting threads
14. Runs a function in a separate thread and
set its result to future:
• step back to naive approach:
configuration_storage db;
future<configuration> config = async([&db]{
configuration config;
db.load(config);
return config;
}); // run loading config in a separate thread
device target_device;
target_device.initialize();
// continue device initialization in current thread
target_device.configure(config.get());
15. template< class Function, class... Args>
result_type async(Function&& f, Args&&... args);
template< class Function, class... Args >
result_type async(launch policy, Function&& f, Args&&... args);
async launch policy:
launch::async – launch a new thread for execution;
launch::deferred – defers the execution until the
returned future value accessed; performs execution in
current thread;
launch::async|launch::deferred – used by default,
allows the implementation to choose one.
16. configuration_storage db;
future<configuration> config = async([&db]{
configuration config;
db.load(config);
return config;
}); // run loading config in a separate thread
device target_device;
if (!target_device.initialize()) {
return; // what will happen to config here ???
}
target_device.configure(config.get());
[3.6.8/5]:
• … If the implementation chooses the launch::async policy,
- a call to a waiting function on an asynchronous return object that shares
the shared state created by this async call shall block until the
associated thread has completed, as if joined;
…
17. provides better control over execution;
does not execute threads itself;
provides a wrapper on a function or a callable object for
store returned value/exception in a future.
configuration_storage db;
packaged_task<configuration ()> load_config([&db] {
configuration config;
db.load(config);
return config;
});
future<configuration> config = load_config.get_future();
// just get future<> for future :)
load_config(); // loading config goes here
device target_device;
target_device.initialize();
target_device.configure(config.get());
18. Implementation for async_exec_service:
class async_exec_service {
queue<function<void ()>> _exec_queue;
// worker threads will take functions from _exec_queue
// ...
void enqueue(function<void ()> task) {/* put task to _exec_queue */}
public:
template <typename _Function>
auto exec(_Function&& f) -> future<decltype(f())>
{
typedef decltype(f()) _Result;
shared_ptr<packaged_task<_Result()>> task =
make_shared<packaged_task<_Result()>>(f);
function<void()> task_wrapper = [task]{(*task)();};
enqueue(task_wrapper);
return task->get_future();
}
// ...
};
19. provides the highest level of control over the shared
state;
does not require a function or callable object for populate
shared state;
requires executing thread explicitly set value/exception to
shared state.
20. Yet another implementation for async_exec_service:
class async_exec_service {
queue<function<void ()>> _exec_queue;
// worker threads will take functions from _exec_queue
// ...
void enqueue(function<void ()> task) {/* put task to _exec_queue */}
public:
template <typename _Function>
auto exec(_Function&& f) -> future<decltype(f())>
{
typedef decltype(f()) _Result;
shared_ptr<promise<_Result>> result =
make_shared<promise<_Result>>();
function<void()> task_wrapper = [result, f]{
try {
result->set_value(f());
} catch (...) {
result->set_exception(current_exception());
}
}
enqueue(task_wrapper);
return result->get_future();
}
};
22. Currently is not a part of C++ Standard
Going to be included in C++17 (N3857)
Already available in boost
23. compose two futures by declaring one to be the
continuation of another:
device target_device;
configuration_storage db;
future<void> configure = async([&db]{
configuration config;
db.load(config);
return config;
}).then([&target_device](future<configuration> config){
// JUST FOR EXAMPLE:
// config is ready at this point,
// but it doesn’t mean we could already configure the device
});
target_device.initialize();
24. wait a number of futures for at least one to be
ready:
device target_device;
configuration_storage db;
configuration config;
future<bool> tasks[] = {
async([&target_device]{ return target_device.initialize(); }),
async([&db, &config]{ db.load(config); return true; }),
};
future<vector<future<bool>>> anyone = when_any(begin(tasks), end(tasks));
future<bool> anyone_completed = anyone.then(
[](future<vector<future<bool>>> lst) {
// JUST FOR EXAMPLE
for (future<bool>& f: lst) {
if (f.is_ready())
return f.get(); // won't block here
}
});
25. wait for a number of futures to be ready:
device target_device;
configuration_storage db;
future<bool> init_device = async([&target_device]{
return target_device.initialize();
});
future<configuration> load_config = async([&db]{
configuration config;
db.load(config);
return config;
});
future<tuple<future<bool>, future<configuration>>> init_all =
when_all(init_device, load_config);
future<bool> device_ready = init_all.then(
[&target_device](tuple<future<bool>, future<configuration>> params){
bool hw_ok = get<0>().get();
if (!hw_ok) return false;
configuration config = get<1>().get();
target_device.configure(config);
return true;
}
);