Иван Пузыревский — Введение в асинхронное...
DESCRIPTION
Доклад посвящен основам асинхронного программирования. Мы кратко обсудим историю вопроса: что такое асинхронность, где, почему и зачем она используется. Затем рассмотрим наиболее частые способы построения асинхронных интерфейсов: основанные на callback'ах и на future/promise. В ходе доклада выделим основные используемые концепции, посмотрим на их реализацию и примеры использования. А в конце поговорим о сложностях, которые часто встречаются в асинхронном программировании.TRANSCRIPT
Асинхронное программированиев C++
Пузыревский Иван, старший разработчик C++ Party, 27.03.2014
Что такое синхронность?
• Синхронное вычисление – блокирующее вызывающий поток исполнения до момента завершения работы вызываемого
• 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;}
Что такое асинхронность?
• Асинхронный ввод/вывод – форма ввода/вывода, допускающая продолжение вычисления до окончания передачи данных
• 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.”
• Можно обслуживать другие сокеты во время недоступности; обрабатывать несколько запросов “одновременно”
Что такое асинхронность?
• Асинхронные события – происходящие независимо от основного потока исполнения
• RPC = Remote Procedure Call – Можно делать полезную работу после отправки запроса
• AJAX = Asynchronous Javascript and XML – Пользователь может взаимодействовать с интерфейсом во время обработки формы/клика
Что такое асинхронность?
• Асинхронное вычисление – с разделенными точками начала и завершения, причем точка завершения не связана с основным потоком исполнения
• Два основных вопроса – Как узнать о завершении вычисления? – Как возвращать значение по завершению вычисления?
Почему асинхронность?
• Две наиболее распространенных реализации: • вынесение “тяжелых” вызовов в отдельные потоки, • использование реактивной модели (epoll/kqueue)
• Во всех случаях есть фазы ожидания событий (доступность сокета на чтение/запись чтение)и реакции на события (чтение ответа/формирование запроса)
• Разница в том, кто планирует исполнение следующего фрагмента кода (ОС в случае потоков, разработчик в случае реактивной модели)
О реактивной модели
• Почему высоконагруженные системы пишут с использованием реактивной модели? – “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)
– Возможность кооперации внутри приложения – Больший контроль над планированием
О реактивной модели
• Есть два похожих, но разных понятия – конкурентность (concurrency) и параллелизм (parallelism)
• Конкурентность – композиция независимо исполняемых вычислений – как справляться с несколькими действиями одновременно?
– относится к структуре программы • Параллелизм – одновременное исполнение (возможно связанных) вычислений – как делать несколько действий одновременно? – относится к исполнению программы
Выражение асинхронности
• Как узнать о завершении вычисления? • Как возвращать значение по завершению вычисления? • 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-модели
Выражение асинхронности
• Основные проблемы – плохое разделение зон ответственности – плохие возможности по композиции
• Мысленный эксперимент – читаем исходный запрос из сокета
(OnRequestReceived) – шлем дополнительный запрос серверу
(QueryBackend + OnBackendResponse) – формируем ответ
(OnRequestHandled) – теперь добавим дисковое кеширование в эту цепочку…
Future/Promise
• Специальный контейнер, представляющий отложенное значение – Фьюча (future) – интерфейс чтения (отложенного) значения
– Промис (promise) – интерфейс “возврата” значения • Асинхронное вычисление знает промис, который она обещает заполнить по окончании вычисления
• Пользователь значет фьючу, которая в каком-то будущем будет заполнена
Future/Promise
• template <class T> class Future<T> { bool IsSet() const; const T& Get() const; T* TryGet() const; void Subscribe(std::function<void(T)> cb); Future<R> Apply( std::function<R(T)> f); Future<R> Apply( std::function<Future<R>(T)> f);};
Future/Promise
• template <class T> class Promise<T> { bool IsSet() const; void Set(const T& value); bool TrySet(const T& value); Future<T> ToFuture() const;};
• // Создает новый (пустой) промисPromise<T> NewPromise();// Создает новую фьючу с заданным значениемFuture<T> NewFuture(const T& value);
Композиция Future
• template <class T> template <class R>Future<R> Future<T>::Apply( std::function<R(T)> f){ auto promise = NewPromise<T>(); this->Subscribe([promise] (T t) { auto result = f(t); promise.Set(result); }); return promise.ToFuture();}
Композиция Future
• template <class T> template <class R>Future<R> Future<T>::Apply( std::function<Future<R>(T)> f){ auto promise = NewPromise<R>(); this->Subscribe([promise] (T t) { auto result = f(t); result.Subscribe([promise] (R r) { promise.Set(r); }); }); return promise.ToFuture();}
Композиция Future
Примеры Future
• Future<int> value = AsyncGetValue();value.Subscribe([] (int v) { std::cerr << “Value is: “ << v << std::endl;});
• Future<int> anotherValue = value .Apply([] (int v) { return 2 * v; });
• Future<int> yetAnotherValue = value .Apply([] (int v) { return 2 * v; }) .Apply([] (int u) { return u + 1; }) .Apply([] (int w) { return w * w; });
Примеры Future
• // Вызывает cb когда вычисление закончитсяvoid DoHeavyStuff(std::function<void()> cb);
• // Возвращает фьючуFuture<void> AsyncDoHeavyStuff() { auto promise = NewPromise<void>(); DoHeavyStuff([promise] () { promise.Set(); }); return promise.ToFuture();}
О запуске вычислений
• До этого мы не касались вопросов, в каком потоке запускаются вычисления и в где исполняются обработчики
• Удобно иметь интерфейс для запуска вычислений • typedef Function<void()> Action; struct Scheduler { virtual void Schedule(Action cb) = 0; };
• Возможные реализации: – синхронный вызов – отдельный поток с очередью – поток с очередью с приоритетами – пул потоков с общей очередью
О запуске вычислений
• Часто нужно исполнить действие в конкретном потоке • Function<void()>Function<void()>::Via(Scheduler* scheduler) { return [=] () { scheduler->Schedule(*this); });}
• // OnDone будет вызван в потоке вычисленияDoHeavyStuff().Subscribe( Bind(&OnDone));// OnDone будет вызван в специальном потокеDoHeavyStuff().Subscribe( Bind(&OnDone).Via(specificThreadScheduler));
О запуске вычислений
• Часто нужно делегировать вычисление в другой поток • Function<Future<T>()>Function<T>()>::AsyncVia(Scheduler* scheduler){ return [=] () { auto promise = NewPromise<T>(); scheduler->Schedule([=] () { promise.Set(this->Run()); }); return promise.ToFuture(); };}
О запуске вычислений
• int GetNthPiDigit(int n);Bind(&GetNthPiDigit) // Function<int(int)> .AsyncVia(workerPool) // Function<Future<int>(int)> .Run(100) // Future<int> .Subscribe( Bind(&OnDone) .Via(controlThread));
О запуске вычислений
int GetNthPiDigit(int n);Bind(&GetNthPiDigit) .AsyncVia(workerPool) .Run(100) .Subscribe( Bind(&OnDone) .Via(controlThread));
Пример
• Future<string> SendByNetwork();string EncodeRequest(Request req);Response DecodeResponse(string blob);
• Future<Response> AskByNetwork(Request req) { auto blob = MakeFuture(EncodeRequest(req)); return blob .Apply(&SendByNetwork) .Apply(&DecodeResponse); }
Пример
• Future<string> SendByNetwork();string EncodeRequest(Request req);Response DecodeResponse(string blob);Scheduler* workerPool;
• Future<Response> AskByNetwork(Request req) { auto blob = Bind(&EncodeRequest).AsyncVia(workerPool).Run(req); return blob .Apply(&SendByNetwork) .Apply(Bind(&DecodeResponse).AsyncVia(workerPool));}
Пример
• Future<Request> GetRequest();Future<Payload> QueryBackend(Request req);Future<Response> HandlePayload(Payload pld);Future<void> Reply(Request req, Response rsp);
• GetRequest() .Subscribe([] (Request req) { auto rsp = QueryBackend(req) .Apply(&HandlePayload) .Apply(Bind(&Reply, req)); }
Все равно не совсем удобно
• Удобная композиция, но еще не идеально • Если проводить аналогию, то хочется исполнять
(псевдо)синхронный код в (псевдо)потоке • Сопрограммы позволяют реализовать псевдопотоки в пользовательском пространстве и сделать код псевдосинхронным
• Хочется магиюT WaitFor(Future<T> future);
• auto req = WaitFor(GetRequest());auto pld = WaitFor(QueryBackend(req));auto rsp = WaitFor(HandlePayload(pld));WaitFor(Reply(req, rsp));
• Об этом следующий доклад
Обработка ошибок
• template <class T> class ErrorOr<T> { ErrorOr<T>(T value); ErrorOr<T>(std::exception_ptr ex); const T& GetOrThrow() const;}
• Function<ErrorOr<T>()>Function<T>()>::Guarded() { try { return ErrorOr(this->Run()); } catch (…) { return ErrorOr(std::current_exception()); }}