(directed acyclic graph, DAG), который описывает, какие подзадачи зависят друг от друга, чтобы выполнить задачу более высокого уровня. Для примера предположим, что хотим создать строку
"foo bar foo bar this that"
, и можем сделать это только путем создания отдельных слов и их объединения с другими словами или с самими собой. Предположим, что этот механизм предоставляется тремя примитивными функциями create
, concat
и twice
.Принимая это во внимание, можно нарисовать следующий DAG, который визуализирует зависимости между ними, что позволяет получить итоговый результат (рис. 9.4).
При реализации данной задачи в коде понятно, что все можно реализовать последовательно на одном ядре ЦП. Помимо этого, все подзадачи, которые не зависят от других подзадач или зависят от уже завершенных, могут быть выполнены конкурентно на нескольких ядрах ЦП.
Может показаться утомительным писать подобный код, даже с помощью
std::async
, поскольку зависимости между подзадачами нужно смоделировать. В этом примере мы реализуем две небольшие вспомогательные функции, которые позволяют преобразовать нормальные функции create
, concat
и twice
в функции, работающие асинхронно. С их помощью мы найдем действительно элегантный способ создать граф зависимостей. Во время выполнения программы граф сам себя распараллелит, чтобы максимально быстро получить результат.
Как это делается
В этом примере мы реализуем некие функции, которые симулируют сложные для вычисления задачи, зависящие друг от друга, и попробуем максимально их распараллелить.
1. Сначала включим все необходимые заголовочные файлы и объявим пространство имен
std
:
#include
#include
#include
#include
#include
#include
using namespace std;
using namespace chrono_literals;
2. Нужно синхронизировать конкурентный доступ к
cout
, поэтому задействуем вспомогательный класс, который написали в предыдущей главе:
struct pcout : public stringstream {
static inline mutex cout_mutex;
~pcout() {
lock_guard l {cout_mutex};
cout << rdbuf();
cout.flush();
}
};
3. Теперь реализуем три функции, которые преобразуют строки. Первая функция создаст объект типа
std::string
на основе строки, созданной в стиле C. Мы приостановим его на 3 секунды, чтобы симулировать сложность создания строки:
static string create(const char *s)
{
pcout{} << "3s CREATE " << quoted(s) << '\n';
this_thread::sleep_for(3s);
return {s};
}
4. Следующая функция принимает два строковых объекта и возвращает их сконкатенированный вариант. Мы будем приостанавливать ее на 5 секунд, чтобы симулировать сложность выполнения этой задачи:
static string concat(const string &a, const string &b)
{
pcout{} << "5s CONCAT "
<< quoted(a) << " "
<< quoted(b) << '\n';
this_thread::sleep_for(5s);
return a + b;
}
5. Последняя функция, наполненная вычислениями, принимает строку и конкатенирует ее с самой собой. На это потребуется 3 секунды:
static string twice(const string &s)
{
pcout{} << "3s TWICE " << quoted(s) << '\n';
this_thread::sleep_for(3s);
return s + s;
}
6. Теперь можно использовать эти функции в последовательной программе, но мы же хотим элегантно ее распараллелить! Так что реализуем некоторые вспомогательные функции. Будьте внимательны, следующие три функции выглядят действительно сложными. Функция
asynchronize
принимает функцию f
и возвращает вызываемый объект, который захватывает ее. Можно вызвать данный объект, передав ему любое количество аргументов, и он захватит их вместе с функцией f
в другой вызываемый объект, который будет возвращен. Этот последний вызываемый объект может быть вызван без аргументов. Затем он вызывает функцию f
асинхронно со всеми захваченными им аргументами:
template
static auto asynchronize(F f)
{
return [f](auto ... xs) {
return [=] () {
return async(launch::async, f, xs...);
};
};
}
7. Следующая функция будет использоваться функцией, которую мы объявим на шаге 8. Она принимает функцию
f
и захватывает ее в вызываемый объект; его и возвращает. Данный объект можно вызвать с несколькими объектами типа future
. Затем он вызовет функцию .get()
для всех этих объектов, применит к ним функцию f
и вернет результат:
template
static auto fut_unwrap(F f)
{
return [f](auto ... xs) {
return f(xs.get()...);
};
}
8. Последняя вспомогательная функция также принимает функцию
f
. Она возвращает вызываемый объект, который захватывает f
. Такой вызываемый объект может быть вызван с любым количеством аргументов, представляющих собой вызываемые объекты, которые он возвращает вместе с f
в другом вызываемом объекте. Этот итоговый вызываемый объект можно вызвать без аргументов. Он вызывает все вызываемые объекты, захваченные в наборе xs...
. Они возвращают объекты типа future
, которые нужно распаковать с помощью функции fut_unwrap
. Распаковка объектов типа future
и применение самой функции f
для реальных значений, находящихся в объектах типа future
, происходит асинхронно с помощью std::async
:
template
static auto async_adapter(F f)
{
return [f](auto ... xs) {
return [=] () {
return async(launch::async,
fut_unwrap(f), xs()...);
};
};
}
9. О’кей, возможно, предыдущие фрагменты кода были несколько запутанными и напоминали фильм «Начало» из-за лямбда-выражений, возвращающих лямбда-выражения. Мы подробно рассмотрим этот вуду-код далее. Теперь возьмем функции
create
, concat
и twice
и сделаем их асинхронными. Функция async_adapter
заставляет обычную функцию ожидать получения аргументов типа future
и возвращает в качестве результата объект типа future
. Она похожа на оболочку, преобразующую синхронный мир в асинхронный. Необходимо использовать функцию asynchronize
для функции create
, поскольку она будет возвращать объект типа future
, но следует передать ей реальные значения. Цепочка зависимостей для задач должна начинаться с вызовов create
:
int main()
{
auto pcreate (asynchronize(create));
auto pconcat (async_adapter(concat));