примитивными функциями create, concat и twice.
Принимая это во внимание, можно нарисовать следующий DAG, который визуализирует зависимости между ними, что позволяет получить итоговый результат (рис. 9.4).
При реализации данной задачи в коде понятно, что все можно реализовать последовательно на одном ядре ЦП. Помимо этого, все подзадачи, которые не зависят от других подзадач или зависят от уже завершенных, могут быть выполнены конкурентно на нескольких ядрах ЦП.
Может показаться утомительным писать подобный код, даже с помощью std::async, поскольку зависимости между подзадачами нужно смоделировать. В этом примере мы реализуем две небольшие вспомогательные функции, которые позволяют преобразовать нормальные функции create, concat и twice в функции, работающие асинхронно. С их помощью мы найдем действительно элегантный способ создать граф зависимостей. Во время выполнения программы граф сам себя распараллелит, чтобы максимально быстро получить результат.
Как это делается
В этом примере мы реализуем некие функции, которые симулируют сложные для вычисления задачи, зависящие друг от друга, и попробуем максимально их распараллелить.
1. Сначала включим все необходимые заголовочные файлы и объявим пространство имен std:
#include <iostream>
#include <iomanip>
#include <thread>
#include <string>
#include <sstream>
#include <future>
using namespace std;
using namespace chrono_literals;
2. Нужно синхронизировать конкурентный доступ к cout, поэтому задействуем вспомогательный класс, который написали в предыдущей главе:
struct pcout : public stringstream {
static inline mutex cout_mutex;
~pcout() {
lock_guard<mutex> 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 <typename F>
static auto asynchronize(F f)
{
return [f](auto ... xs) {
return [=] () {
return async(launch::async, f, xs...);
};
};
}
7. Следующая функция будет использоваться функцией, которую мы объявим на шаге 8. Она принимает функцию f и захватывает ее в вызываемый объект; его и возвращает. Данный объект можно вызвать с несколькими объектами типа future. Затем он вызовет функцию .get() для всех этих объектов, применит к ним функцию f и вернет результат:
template <typename F>
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 <typename F>
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));
auto ptwice (async_adapter(twice));
10. Теперь у нас есть функции, которые распараллеливаются автоматически и имеют такие же имена, что и оригинальные функции, но с префиксом p. Далее создадим сложное дерево зависимостей. Сначала добавим строки "foo " и "bar ", которые мгновенно сконкатенируем в "foo bar ". Эта строка будет сконкатенирована сама с собой с помощью функции twice. Затем создадим строки "this " и "that ", которые сконкатенируем в "this that ". Наконец, сконкатенируем все эти строки в "foo bar foo bar this that ". Результат будет сохранен в переменной