Асинхронное программирование на С++: callbacks, futures, fibers.
Павел Сушин, старший разработчик
Группа разработки технологий распределенных вычислений
Чем я занимаюсь
• Отказоустойчивое, консистентное, транзакционное хранилище.
• Несколько слоев хранения, с разными гарантиями на throughput и latency.
• Вычисления в модели Map-Reduce поверх этих данных.
• Серверная часть – 100% С++
• ~10 кластеров, ~2500 машин, ~25 Pb данных, сотни пользователей.
3
YT – распределенная система хранения и обработки данных.
Принцип №2*
6
lock-free queue
Thread
callbackcallback
SingleThreadInvoker
• Тред + очередь• Пул тредов + очередь• Тред + несколько очередей с
приоритетами • …
struct IInvoker { virtual void Invoke(callback) = 0; };
Callbacks
8
struct IAction { virtual void Run() = 0; };
template <class R> struct IFunc { virtual R Run() = 0; };
template <class R, class T> struct IParamFunc { virtual R Run(T t) = 0; };
template <class T> struct IParamAction { virtual void Run(T t) = 0; };
Callbacks
9
class TRequest; class TResponse; void SendMessage( TRequest req, IParamAction<TResponse> response);
class TResponseHandler : public IParamAction<TResponse> { public: TResponseHandler(TRequest req); void Run(TResponse rsp); };
... TRequest req; TResponseHandler handler(req); SendMessage(req, handler);...
Callbacks
11
int Sum(int a, int b) { return a + b; }
void CallAndPrint(TCallback<void(int)> f) { std::cout << f.Run() << std::endl; }
auto f = BIND(&Sum, 1); auto g = BIND(f, 2);
CallAndPrint(g);
Почему не std::function?• Видно в RefCountedTracker
• BIND - сохраняет информацию о точке связывания
• Compile-time проверки
12
class T : public TRefCounted { ... void Foo() { BIND(&T::Foo, this); // Compile-time error BIND(&T::Foo, MakeStrong(this)); BIND(&T::Foo, MakeWeak(this)); } };
Callbacks
13
class TRequest; class TResponse;
void SendRequest( TRequest req, TCallback<void(TResponse)> callback);
void ResponseHandler(TRequest req, TResponse rsp);
...TRequest req; SendRequest(req, BIND(ResponseHandler, req));
В каком потоке будет вызван ResponseHandler?
TCallback::Via
14
TCallback<void()> TCallback<void()>::Via(IInvoker* invoker) { return BIND([=]() { invoker->Invoke(*this); }); }
IInvoker* invoker;TRequest req; SendRequest( req, BIND(ResponseHandler, req).Via(invoker));
Привязываем запуск TCallback к конкретному инвокеру
Callbacks: проблемы
• Плохие возможности по композиции:
• как дождаться окончания двух асинхронных действий?
• как дважды подписаться на одно событие?
• как составлять цепочки асинхронных вычислений?
• Акцент на коллбеках, а не на результате действия.
15
Future/Promise
17
template <class T> class TFuture { public: bool IsSet() const; T Get() const;
void Subscribe(TCallback<void(T)> cb);
};
template <class T> class TPromise { public: TFuture<T> ToFuture() const; void Set(T t); };
TSet GetPromise Future
Future/Promise
18
Future смещает акцент с вызова коллбека на результат асинхронного вычисления.
void DoHeavyStuff(TCallback<void()> cb);
TFuture<void> AsyncDoHeavyStuff() { auto promise = NewPromise<void>(); DoHeavyStuff([promise] () { promise.Set(); }); return promise.ToFuture(); }
Future/Promise
19
class TRequest; class TResponse;
TFuture<TResponse> SendRequest(TRequest req);
void ResponseHandler(TRequest req, TResponse rsp);
... IInvoker* invoker; TRequest req; SendRequest(req).Subscribe( BIND(ResponseHandler, req) .Via(invoker));
TCallback::AsyncVia
20
Как делегировать вычисление в другой поток?
TCallback<TFuture<T>()> TCallback<T>()>::AsyncVia(IInvoker* invoker) { return BIND([=] () { auto promise = NewPromise<T>(); invoker->Invoke(BIND([=] () { promise.Set(this->Run()); })); return promise.ToFuture(); }); }
TCallback + TFuture
21
int GetNthPiDigit(int n);
BIND(&GetNthPiDigit) // TCallback<int(int)> .AsyncVia(workerPool) // TCallback<TFuture<int>(int)> .Run(100) // TFuture<int> .Subscribe( BIND(&OnDone) .Via(controlThread));
TFuture::Apply
22
template <class T> class TFuture<T> { bool IsSet() const; const T& Get() const;
void Subscribe(TCallback<void(T)> cb); TFuture<R> Apply(TCallback<R(T)> f); TFuture<R> Apply(TCallback<TFuture<R>(T)> f); };
Как связывать асинхронные действия в цепочки?
TFuture::Apply
23
template <class T> template <class R> TFuture<R> TFuture<T>::Apply(TCallback<R(T)> f) { auto promise = NewPromise<R>(); this->Subscribe(BIND([promise] (T t) { auto result = f(t); promise.Set(result); })); return promise.ToFuture(); }
TFuture::Apply
24
TFuture<int> value = AsyncGetValue();
value.Subscribe(BIND([] (int v) { std::cerr << “Value is: “ << v << std::endl; }));
TFuture<int> anotherValue = value .Apply(BIND([] (int v) { return 2 * v; }));
TFuture<int> yetAnotherValue = value .Apply(BIND([] (int v) { return 2 * v; })) .Apply(BIND([] (int u) { return u + 1; })) .Apply(BIND([] (int w) { return w * w; }));
TCallback + TFuture
25
TFuture<string> SendByNetwork(string block); string EncodeRequest(TRequest req); TResponse DecodeResponse(string blob); IInvoker* workerPool;
TFuture<TResponse> SendMessage(TRequest req) { auto futureBlob = BIND(&EncodeRequest, req) .AsyncVia(workerPool) .Run();
return futureBlob .Apply(&SendByNetwork) .Apply(BIND(&DecodeResponse).AsyncVia(workerPool)); }
Обработка ошибок
26
template <class T> class TErrorOr<T> { TErrorOr<T>(T value); TErrorOr<T>(std::exception ex); const T& GetOrThrow() const; }
TCallback<TErrorOr<T>()> TCallback<T>()>::Guarded() { try { return TErrorOr(this->Run()); } catch (…) { return TErrorOr(std::current_exception()); } }
Callbacks + Futures: проблемы
27
• Как прерывать цепочки при возникновении ошибок?• Как запускать многостадийные асинхронные
вычисления со сложной логикой перехода между стадиями?
Хочется «склеивать» асинхронные вычисления в (псевдо)синхронном стиле:
T WaitFor(TFuture<T> future);
Значит нужно самостоятельно планировать (псевдо)потоки, в которых выполняются (псевдо)синхронные вычисления.
Fibers
28
TFuture<string> SendByNetwork(string block); string EncodeRequest(TRequest req); TResponse DecodeResponse(string blob); IInvoker* workerPool;
TFuture<TResponse> SendMessage(TRequest req) { auto reqBlob = WaitFor(BIND(&EncodeRequest, req) .AsyncVia(workerPool) .Run());
auto rspBlob = WaitFor(SendByNetwork(reqBlob)); return BIND(&DecodeResponse, rspBlob) .AsyncVia(workerPool) .Run(); }
Fibers
• Контекст = состояние регистров
• Нужно уметь сохранять и переключать контекст (например использовать libcoro)
• Fiber = контекст + стек
• У треда есть очередь (готовых) файберов и собственный контекст, где он разгребает эту очередь (scheduling context)
• Текущий файбер разгребает очередь коллбеков
29
Fibers
30
• При вызове WaitFor:
• переходим в scheduling context;
• подписываем на future добавление файбера в очередь готовых;
• достаем очередной готовый файбер из очереди (или создаем новый) и ставим на исполнение
Overview
31
struct IInvoker { virtual void Invoke(TCallback<void()>) = 0; };
template <class R, class...TArgs> class TCallback { public: ... TCallback<R(...TArgs)> Via(IInvoker* invoker); TCallback<TFuture<R>(...TArgs)> AsyncVia(IInvoker* invoker); }
Overview
32
template <class T> class TFuture<T> { bool IsSet() const; const T& Get() const;
void Subscribe(TCallback<void(T)> cb); TFuture<R> Apply(TCallback<R(T)> f); TFuture<R> Apply(TCallback<TFuture<R>(T)> f); };
T WaitFor(TFuture<T> future);
Спасибо!
+7 916 9529490
Павел Сушин, старший разработчик
Группа разработки технологий распределенных вычислений
http://facebook.com/yandex.ev
ents
http://twitter.com/ya_events