Доклад посвящен основам асинхронного программирования. Мы кратко обсудим историю вопроса: что такое асинхронность, где, почему и зачем она используется. Затем рассмотрим наиболее частые способы построения асинхронных интерфейсов: основанные на callback'ах и на future/promise. В ходе доклада выделим основные используемые концепции, посмотрим на их реализацию и примеры использования. А в конце поговорим о сложностях, которые часто встречаются в асинхронном программировании.
3. Что такое синхронность?
• Синхронное вычисление – блокирующее вызывающий
поток исполнения до момента завершения работы
вызываемого
• int main() { /*(1)*/ foo(42); /*(6)*/ }
int foo(int x) {
/*(2)*/
int y = x * x;
int z = bar(y);
/*(5)*/
return z;
}
int bar(int y) {
/*(3)*/
int w = 42 + y;
/*(4)*/
return w;
}
4. Что такое асинхронность?
• Асинхронный ввод/вывод – форма ввода/вывода,
допускающая продолжение вычисления до окончания
передачи данных
• std::string blob(100);
int rv = read(fd, blob.data(), blob.size());
• “If successful, the number of bytes actually read is returned.
Upon reading end-of-file, zero is returned. Otherwise, a -1 is
returned and the global variable errno is set to indicate the
error. […] EAGAIN – The file was marked for non-blocking I/O,
and no data were ready to be read.”
• Можно обслуживать другие сокеты во время
недоступности; обрабатывать несколько запросов
“одновременно”
5. Что такое асинхронность?
• Асинхронные события – происходящие независимо от
основного потока исполнения
• RPC = Remote Procedure Call
– Можно делать полезную работу после отправки запроса
• AJAX = Asynchronous Javascript and XML
– Пользователь может взаимодействовать с
интерфейсом во время обработки формы/клика
6. Что такое асинхронность?
• Асинхронное вычисление – с разделенными точками
начала и завершения, причем точка завершения не
связана с основным потоком исполнения
• Два основных вопроса
– Как узнать о завершении вычисления?
– Как возвращать значение по завершению вычисления?
7. Почему асинхронность?
• Две наиболее распространенных реализации:
• вынесение “тяжелых” вызовов в отдельные потоки,
• использование реактивной модели (epoll/kqueue)
• Во всех случаях есть фазы ожидания событий
(доступность сокета на чтение/запись чтение)
и реакции на события (чтение ответа/формирование
запроса)
• Разница в том, кто планирует исполнение следующего
фрагмента кода (ОС в случае потоков, разработчик в
случае реактивной модели)
8. О реактивной модели
• Почему высоконагруженные системы пишут с
использованием реактивной модели?
– “Majority of the context-switching cost attributable to the
complexity of the scheduling decision by a modern SMP
CPU scheduler”
(Paul Turner, Google, at Linux Plumbers Conference 2013)
– Возможность кооперации внутри приложения
– Больший контроль над планированием
9. О реактивной модели
• Есть два похожих, но разных понятия –
конкурентность (concurrency) и параллелизм (parallelism)
• Конкурентность – композиция независимо исполняемых
вычислений
– как справляться с несколькими действиями
одновременно?
– относится к структуре программы
• Параллелизм – одновременное исполнение (возможно
связанных) вычислений
– как делать несколько действий одновременно?
– относится к исполнению программы
10. Выражение асинхронности
• Как узнать о завершении вычисления?
• Как возвращать значение по завершению вычисления?
• Pull: дескриптор и способ его инспекции
read(fd) = EAGAIN + epoll_ctl() + epoll_wait()
• Push: функция-обработчик (callback; проактивная)
// JS
$.ajax({ … success: function() { console.log(“Done!”); } })
// C++
typedef void (*uv_alloc_cb)(uv_handle_t* h, size_t hint, uv_buf_t* buf);
typedef void (*uv_read_cb)(uv_stream_t* s, ssize_t nread, const uv_buf_t* buf);
int uv_read_start(uv_stream_t*, uv_alloc_cb alloc_cb, uv_read_cb read_cb);
• Далее доклад будет о push-модели
11. Выражение асинхронности
• Основные проблемы
– плохое разделение зон ответственности
– плохие возможности по композиции
• Мысленный эксперимент
– читаем исходный запрос из сокета
(OnRequestReceived)
– шлем дополнительный запрос серверу
(QueryBackend + OnBackendResponse)
– формируем ответ
(OnRequestHandled)
– теперь добавим дисковое кеширование в эту цепочку…
12. Future/Promise
• Специальный контейнер, представляющий отложенное
значение
– Фьюча (future) – интерфейс чтения (отложенного)
значения
– Промис (promise) – интерфейс “возврата” значения
• Асинхронное вычисление знает промис, который она
обещает заполнить по окончании вычисления
• Пользователь значет фьючу, которая в каком-то будущем
будет заполнена
20. О запуске вычислений
• До этого мы не касались вопросов, в каком потоке
запускаются вычисления и в где исполняются обработчики
• Удобно иметь интерфейс для запуска вычислений
• typedef Function<void()> Action;
struct Scheduler {
virtual void Schedule(Action cb) = 0;
};
• Возможные реализации:
– синхронный вызов
– отдельный поток с очередью
– поток с очередью с приоритетами
– пул потоков с общей очередью
21. О запуске вычислений
• Часто нужно исполнить действие в конкретном потоке
• Function<void()>
Function<void()>::Via(Scheduler* scheduler) {
return [=] () {
scheduler->Schedule(*this);
});
}
• // OnDone будет вызван в потоке вычисления
DoHeavyStuff().Subscribe(
Bind(&OnDone));
// OnDone будет вызван в специальном потоке
DoHeavyStuff().Subscribe(
Bind(&OnDone).Via(specificThreadScheduler));
22. О запуске вычислений
• Часто нужно делегировать вычисление в другой поток
• Function<Future<T>()>
Function<T>()>::AsyncVia(Scheduler* scheduler)
{
return [=] () {
auto promise = NewPromise<T>();
scheduler->Schedule([=] () {
promise.Set(this->Run());
});
return promise.ToFuture();
};
}
23. О запуске вычислений
• int GetNthPiDigit(int n);
Bind(&GetNthPiDigit)
// Function<int(int)>
.AsyncVia(workerPool)
// Function<Future<int>(int)>
.Run(100)
// Future<int>
.Subscribe(
Bind(&OnDone)
.Via(controlThread));
24. О запуске вычислений
int GetNthPiDigit(int n);
Bind(&GetNthPiDigit)
.AsyncVia(workerPool)
.Run(100)
.Subscribe(
Bind(&OnDone)
.Via(controlThread));
28. Все равно не совсем удобно
• Удобная композиция, но еще не идеально
• Если проводить аналогию, то хочется исполнять
(псевдо)синхронный код в (псевдо)потоке
• Сопрограммы позволяют реализовать псевдопотоки в
пользовательском пространстве и сделать код
псевдосинхронным
• Хочется магию
T WaitFor(Future<T> future);
• auto req = WaitFor(GetRequest());
auto pld = WaitFor(QueryBackend(req));
auto rsp = WaitFor(HandlePayload(pld));
WaitFor(Reply(req, rsp));
• Об этом следующий доклад