Предлагаемый подход позволяет без труда получить параллельную асинхронную обработку данных без явного использования средств синхронизации, по максимуму задействуя доступные вычислительные ресурсы. Использование сопрограмм значительно упрощает написание многопоточного кода. Это дает возможность сконцентрироваться непосредственно на задаче обработки данных, не занимаясь вопросами синхронизации различных операций, включая асинхронную работу с сетью.
5. 5
Map Возможна параллельная обработка с помощью контейнера для хранения результатов Не требуется синхронизация для обработки, только ожидание завершения стадий Каждая стадия ждет завершения предыдущей Проблемы:
–Агрегация
–Фильтрация
–Работает хорошо только 1 к 1
Парсинг
Преобразование
Обработка
6. 6
Reduce
Агрегация Подсчет количества определенных слов Подсчет статистики по определенным метрикам Суть: уменьшение количества данных, «сжатие» для определенных целей
#1
Reduce
#2
#3
7. 7
Проблемы традиционных подходов Требуется синхронизация Сложный контроль потребления памяти Последовательное выполнение стадий Нагрузка на планировщик Внешние взаимодействия:
–сетевые запросы
–обращение к базе данных
–обращение к диску
9. 9
Что хочется? Эффективно использовать сеть: получение и посылка данных Эффективно использовать процессор: запускать задачи, не дожидаясь завершения стадий Эффективно использовать память: выделять память под конкретные нужды Разгрузка планировщика: планировать более крупные задачи Нацеленность на результат: получение первых конечных данных как можно быстрее По возможности не использовать синхронизацию явно Максимизация параллельности операций Обработка сложных сценариев (например, петли)
11. 11
Обработка данных Преобразование одних данных в другие Data Oriented Design – всё есть преобразование данных
Преобразование
Исходные данные
Результат
12. 12
Диаграмма преобразования данных
Фильтр
Парсинг: URL
Загрузка
Парсинг: текст
Парсинг:
href
Разделение слов
host
path
host
body
url
body
paragraph
word
url
13. 13
Диаграмма преобразования данных
Фильтр
Парсинг: URL
Загрузка
Парсинг:
текст
Парсинг: href
Разделение слов
host
path
host body
url
body
paragraph
Async DNS resolving
Async partial reading
Header parsing
word
url
14. 14
Диаграмма преобразования данных
Фильтр
Парсинг: URL
Загрузка
Парсинг:
текст
Парсинг: href
Разделение слов
host
path
host body
url
body
paragraph
Async DNS resolving
Async partial reading
Header parsing
word
url
url
15. 15
Как реализовать? Обратные связи – традиционные способы не подходят Асинхронность в обработчиках
16. 16
Канал
Парсинг: текст
Разделение слов
paragraph
Канал:
Channel<std::string>
17. 17
Канал: параллельность
Парсинг:
текст
Разделение слов
paragraph
Канал:
Channel<std::string>
Парсинг:
текст
Парсинг:
текст
Разделение слов
18. 18
Канал: интерфейс put – посылка значения get – извлечение значения Потокобезопасный Можно: for (auto&& w: wordsChan) { ... }
template<typename T>
struct Channel
{
bool empty() const;
void put(T val);
bool get(T& val);
T get();
void open();
void close();
};
19. 19
Канал: параллельность
Парсинг:
текст
Разделение слов
paragraph
Канал: Channel<std::string>
Парсинг:
текст
Парсинг:
текст
Разделение слов
Channel::put
Channel::get
20. 20
Канал: параллельность
Парсинг:
текст
Разделение слов
paragraph
Канал:
Channel<std::string>
Парсинг:
текст
Парсинг:
текст
Разделение слов
Channel::put
Channel::get
Channel::close
returns: false
21. 21
Сложности Канал может быть пустым, что делать потребителю? Как обрабатывать асинхронные взаимодействия? Сколько использовать обработчиков для параллельной работы?
22. 22
Решение: сопрограммы! Подпрограмма: выполняются все действия перед возвратом управления Сопрограмма: частичное выполнение действий перед возвратом управления Сопрограмма обобщает понятие подпрограммы, т.е. подпрограмма - это частный случай сопрограммы. Для эффективной реализации на С++ будем использовать boost.context.
24. 24
Обработчики Связь двух каналов
template<typename T_src, typename T_dst, typename F_pipe>
void piping(T_src& s, T_dst& d, F_pipe f, int n = 1)
{
goN(n, [&s, &d, f] {
auto c = closer(d);
f(s, d);
});
}
piping(c1, c2, [](Channel<int>& src, Channel<int>& dst) {
for (int v: src)
dst.put(v + 1);
});
closer: автоматически закрывает канал
goN: асинхронно запускает n сопрограмм
25. 25
Обработчики Связь двух каналов 1 к 1 (map)
template<typename T_src, typename T_dst, typename F_pipe>
void piping1to1(T_src& s, T_dst& d, F_pipe f, int n = 1)
{
piping(s, d, [f] (T_src& s, T_dst& d) {
for (auto&& v: s)
d.put(std::move(f(v)));
}, n);
}
piping1to1(c1, c2, [](int v) {
return v + 1;
}, 10);
будет запущено 10 обработчиков
26. 26
Разделение каналов
go([&] {
auto c1 = closer(contentHref);
auto c2 = closer(contentText);
for (auto&& c: content)
{
contentHref.put(c);
contentText.put(c.second);
}
});
Split
Парсинг: текст
Парсинг:
href
host + body
body
host + body
body
28. 28
Пример обработчика: ссылки Преобразование ссылки из строки в host+path
StrPair parseUrl(const Str& url)
{
static const regex e("http://([wd._-]*[wd_-]+)(.*)", regex::icase);
smatch what;
bool result = regex_search(url, what, e);
if (!result)
return {};
Str host = what[1];
Str path = what[2];
if (path.empty())
path = "/";
return {host, path};
}
piping1to01(filteredUrl, parsedUrl, parseUrl);
piping1to01: игнорирует пустые элементы
29. 29
Пример обработчика: слова Разделение текста на отдельные слова
void splitWords(const Str& text, Channel<Str>& words)
{
static const regex e("([a-zA-Z]+)");
sregex_token_iterator i = make_regex_token_iterator(text, e, 1);
sregex_token_iterator ie;
while (i != ie)
words.put(*i++);
}
piping1toMany(text, words, splitWords);
piping1toMany: один ко многим
30. 30
Петля Помещаем url после разбора снова в фильтр: piping1toMany(contentHref, url, parseHref); Как оборвать петлю?
Фильтр
Парсинг: URL
Загрузка
Парсинг:
href
host
path
host
body
url
url
31. 31
Обработка петель Решение: использовать closeAndWait
int threads = std::thread::hardware_concurrency();
ThreadPool tp(threads, "tp");
Channel<Str> url;
url.put("http://www.boost.org");
closeAndWait(tp, url);
template<typename T_channel>
void closeAndWait(ThreadPool& tp, T_channel& c)
{
tp.wait();
c.close();
tp.wait();
}
засыпание обработчиков на Channel::get
закрываем самый первый канал
выход из всех обработчиков
32. 32
Результат Результат для 1000 страниц http://www.boost.org
Слово
Количество
the
10988
of
4943
to
4838
and
4837
a
4421
boost
3193
for
3136
is
3103
in
2637
library
1951
that
1755
be
1622
as
1358
are
1310
with
1301
c
1260
it
1170
or
1157
this
1095
can
1094
34. 34
Количество обработчиков Какое число обработчиков необходимо в случае:
–Лёгких задач?
–Тяжелых задач?
–Сетевых операций?
35. 35
Количество обработчиков Какое число обработчиков необходимо в случае:
–Лёгких задач ~1-2
–Тяжелых задач = числу ядер
–Сетевых операций >> числа ядер
36. 36
Обработка блокирующих вызовов Каким образом обрабатывать блокирующие вызовы в случае:
–Обращения к диску?
–Синхронные запросы к удаленной БД?
37. 37
Обработка блокирующих вызовов Каким образом обрабатывать блокирующие вызовы в случае:
–Обращения к диску
–Синхронные запросы к удаленной БД
Ответ: запускать в отдельном пуле потоков, число потоков >> числа ядер
41. 41
Кеширование DNS запросов
Загрузка
host + body
host + path
Resolver
host + endpoint
состояние:
host → endpoint
42. 42
Нагрузка на планировщик
Какова нагрузка на планировщик в случае частого обмена сообщениями через каналы?
bool get(T& val) {
Lock lock(mutex);
if (!queue.empty()) {
val = std::move(queue.front());
queue.pop();
return true;
}
if (closed)
return false;
Waiter w(val);
waiters.push(w);
lock.release();
deferProceed([this, &w](Handler proceed) {
w.setProceed(std::move(proceed));
mutex.unlock();
});
return w.hasValue();
}
значение в очереди => сразу возвращаем
создаем ожидающего и добавляем его в список
устанавливаем обработчик для возобновления выполнения
после продолжения значение записывается в переменную val (при наличии)
43. 43
Нагрузка на планировщик
Планировщик используется только при вызове proceed()!
Combining – техника; нет нагрузки в случае простаивания
void put(T val)
{
Lock lock(mutex);
Waiter* w = waiters.pop();
if (w)
{
lock.unlock();
w->proceed(std::move(val));
}
else
{
queue.emplace(std::move(val));
}
}
если есть ожидающий => передаем значение непосредственно ему и возобновляем выполнение
добавляем значение в очередь
аналогично реализуются:
•неблокирующие мьютексы
•неблокирующие condition variables
46. 46
Каналы: ограничение по размеру Реализация: 2 очереди для ожидания вместо одной Ограничивает потребление памяти Сетевое использование: TCP flow control Контролирует latency
47. 47
Обработчики и состояние Отдавайте предпочтение обработчикам без состояний, т.к. они позволяют параллельное выполнение стадии Reduce-стадии как правило имеют состояние Часто можно разбить reduce-стадии на несколько независимых, результат объединить на следующей стадии
49. 49
Буферизированные каналы Позволяют снизить lock contention на каналы Увеличивают throughput Увеличивают latency
50. 50
Планировщик «в глубину» Обычно используют планировщик «в ширину», т.е. очередь задач. Вместо очереди задач можно использовать стек задач. Получаем планировщик «в глубину» с другими свойствами.
51. 51
Планировщик «в глубину»
Преимущества: Более быстрое получение первых результатов Снижение нагрузки на обработчики за счет более равномерного распределения данных между стадиями Уменьшение потребления памяти «Прогретые» процессорные кеши Отлично работает в связке с bounded каналами, заставляя данные более активно перемещаться к результату
Недостатки: Отсутствие fairness Потеря контроля над latency
52. 52
Планы на будущее Распределенность Отказоустойчивость Оптимизация планировщика
54. 54
Предлагаемый подход позволяет Реализовать простые сценарии (фильтрация, кеширование, парсинг, преобразование и т.д.) Реализовать сложные сценарии (петли, split, merge) Использовать асинхронное сетевое взаимодействие Забыть про синхронизацию Простая параллелизация:
–Стадии выполняются параллельно, в отличие от MR
–Несколько обработчиков одной стадии также выполняются параллельно