Асинхронность и сопрограммы

43

Upload: platonov-sergey

Post on 17-Jul-2015

462 views

Category:

Software


1 download

TRANSCRIPT

Page 1: Асинхронность и сопрограммы
Page 2: Асинхронность и сопрограммы

Асинхронность исопрограммы

Демченко Григорий, старший разработчик

C++ User Group, 27 февраля 2015

Page 3: Асинхронность и сопрограммы

План рассказа

Реализация сопрограмм

Ожидающие примитивы

Примитивы, использующие планировщик

Заключение

Page 4: Асинхронность и сопрограммы

План рассказа

Реализация сопрограмм

Ожидающие примитивы

Примитивы, использующие планировщик

Заключение

Page 5: Асинхронность и сопрограммы

Многопоточный серверAcceptor acceptor(80);

while (true) {

socket = acceptor.accept();

go([socket] { // запуск в новом потоке

request = socket.read();

response = handleRequest(request);

socket.write(response);

});

}

• Недостатки:

o Большую часть времени потоки ожидают на I/O событиях и не делают полезной работы => бесполезная трата ресурсов

Page 6: Асинхронность и сопрограммы

Асинхронность

action1();

action2();

• Синхронность: action2 не выполняется, пока не закончится action1.

• Асинхронность: action2 выполняется, даже если action1 не завершился.

Page 7: Асинхронность и сопрограммы

Асинхронность

Когда заканчивается операция?

1. Мы вызываем: периодический опрос состояния - реакторнаямодель (select, epoll, kqueue)

2. Нас вызывают: проакторная модель исполнения (UI, IOCP, javascript):

void onActionComplete(Result) {...}

action1(onActionComplete);

Будем использовать проактор boost.asio

Page 8: Асинхронность и сопрограммы

Асинхронный сервер

Acceptor acceptor(80);

acceptor.onAccept([] (socket) {

socket.onRead([&socket](request) {

response = handleRequest(request);

socket.onWrite([&response]() {

// completed

});

});

// continue accepting???

});

• Более бережное использование ресурсов, но…

Page 9: Асинхронность и сопрограммы

Асинхронный сервер: обсуждение

• Достоинства

o Производительность

o Относительно простая параллелизация

• Недостатки

o Сложность:

Передача контекста

Нелинейная структура кода

Обработка ошибок

Время жизни объектов

Отладка

С ростом количества операций сложность растет нелинейно

Page 10: Асинхронность и сопрограммы

Решение

Что бы хотелось?

• У асинхронного взять быстродействие

• У синхронного - простоту

Т.е. объединить достоинства и убрать недостатки, присущие обоим методам.

Как этого добиться? Ответ: использовать сопрограммы!!!

Page 11: Асинхронность и сопрограммы

Сопрограммы

• Подпрограмма: выполняются все действия перед возвратом управления

• Сопрограмма: частичное выполнение действий перед возвратом управления

Сопрограмма обобщает понятие подпрограммы, т.е. подпрограмма -это частный случай сопрограммы.

Пример сопрограммы: генераторы (например, на python)

Для эффективной реализации на С++ будем использовать метод сохранения контекста исполнения (stackful coroutines).

Page 12: Асинхронность и сопрограммы

Реализация сопрограмм

• Проблемы

o Сопрограммы никак не описаны в стандарте

o Необходимо применять низкоуровневые операции => зависимость от платформы, архитектуры и даже компилятора.

• Решение: использовать boost.context:

o Используется только ассемблер

o Поддержка платформ: Windows, Linux, MacOSX

o Поддержка мобильных платформ: Windows Phone, Android, IOS,

o Поддержка архитектур: x86, x86_64, ARM, Spark, Spark64, PPC, PPC64, MIPS

o Поддержка компиляторов: GCC, Clang, Visual Studio

Page 13: Асинхронность и сопрограммы

boost.context

Реализует 2 функции:

• make_fcontext: создает контекст и инициализирует стек сопрограммы

• jump_fcontext: переходит в новый контекст с сохранением текущего контекста для последующего возврата

Использование:

1. Выделение памяти под стек.

2. Создание контекста

ctx = make_fcontext(stackPtr, stackSize, callback)

3. Переход: jump_fcontext(callerCtx, ctx, ptr)

4. Возврат: jump_fcontext(ctx, callerCtx, ptr)

Page 14: Асинхронность и сопрограммы

Реализация сопрограмм: Coro

void coroFn() {

CLOG("Yielding...");

yield();

CLOG("Returning...");

}

CLOG("Starting...");

Coro c(coroFn);

CLOG("Resuming...");

c.resume();

CLOG("Completed");

Выдача:

0: Starting...1: Yielding...0: Resuming...1: Returning...0: Completed

Page 15: Асинхронность и сопрограммы

Использование сопрограмм

• Переделываем асинхронный вызов:

performAsyncOp(completionAction);

• В синхронный с использованием сопрограмм:

performOp();

void performOp() {

auto& coro = currentCoro();

performAsyncOp([&coro] { coro.resume(); });

yield();

}

Page 16: Асинхронность и сопрограммы

Использование сопрограмм

• Scheduler: boost::asio::io_service

• Thread: std::thread

• Coro: реализация с использованием boost.context

• Network: boost::asio::read/write/...

Page 17: Асинхронность и сопрограммы

Проблема: состояние гонки

Возможно возобновление работы сопрограммы до выхода из нее

Варианты решения:

1. std::mutex

2. boost::asio::strand

3. defer

Page 18: Асинхронность и сопрограммы

Решение: defer

defer: откладывание асинхроннойоперации до выхода из сопрограммы

typedef

std::function<void()> Action;

void defer(Action);

Page 19: Асинхронность и сопрограммы

Использование defer

void performOp() {

auto& coro = currentCoro();

performAsyncOp([&coro] { coro.resume(); });

yield();

}

void performOp() {

auto& coro = currentCoro();

defer([&coro] {

performAsyncOp([&coro] { coro.resume(); });

});

}

Page 20: Асинхронность и сопрограммы

Реализация на сопрограммах

Acceptor acceptor(80);

while (true) {

socket = acceptor.accept();

go([socket] { // запуск в сопрограмме

request = socket.read();

response = handleRequest(request);

socket.write(response);

});

}

• Код полностью идентичен синхронному подходу

Page 21: Асинхронность и сопрограммы

План рассказа

Реализация сопрограмм

Ожидающие примитивы

Примитивы, использующие планировщик

Заключение

Page 22: Асинхронность и сопрограммы

Ожидание завершения операции

• Задача: запустить асинхронно операцию и дождаться её завершения.

void goWait(Action action);

• Проблема обычного подхода: во время ожидания поток остается заблокированным

• Сопрограммы с легкостью решают эту проблему

Page 23: Асинхронность и сопрограммы

Ожидание завершения операции

void goWait(Action action) {

deferProceed([&action](Action proceed) {

go([proceed, &action] { // создаем новую сопрограмму

action();

proceed(); // продолжаем выполнение сопрограммы

});

});

}

void deferProceed(std::function<void(Action)> proceed) {

auto& coro = currentCoro();

defer([&coro, proceed] {

proceed([&coro] { coro.resume(); });

});

}

Page 24: Асинхронность и сопрограммы

Ожидание завершения операций

• Задача: запустить асинхронно несколько операций и дождаться их завершения.

void goWait(std::initializer_list<Action> actions);

• Проблема: во время ожидания поток остается заблокированным

• Сопрограммы с легкостью решают эту проблему

Page 25: Асинхронность и сопрограммы

Ожидание завершения операций

void goWait(std::initializer_list<Action> actions) {

deferProceed([&actions](Action proceed) {

std::shared_ptr<void> proceeder(

nullptr, [proceed](void*) { proceed(); });

for (const auto& action: actions) {

go([proceeder, &action] {

action();

});

}

});

}

Page 26: Асинхронность и сопрограммы

Ожидание завершения операций

Последовательность вызовов для:

goWait(action1, action2);

Page 27: Асинхронность и сопрограммы

Пример: вычисление числа Фибоначчи

int fibo(int v) {

if (v < 2)

return v;

int v1, v2;

goWait({

[v, &v1] { v1 = fibo(v-1); },

[v, &v2] { v2 = fibo(v-2); }

});

return v1 + v2;

}

Page 28: Асинхронность и сопрограммы

План рассказа

Реализация сопрограмм

Ожидающие примитивы

Примитивы, использующие планировщик

Заключение

Page 29: Асинхронность и сопрограммы

Планировщик• Интерфейс планировщика:

struct IScheduler {

virtual void schedule(Action action) = 0;

};

• Планировщик отвечает за запуск задач: action

• ThreadPool реализует интерфейс IScheduler с использованием boost.asio:

struct ThreadPool : IScheduler {

ThreadPool(size_t thread_count) { … }

void schedule(Action action) {

service.post(std::move(action));

}

private:

boost::asio::io_service service;

};

Page 30: Асинхронность и сопрограммы

Возобновление сопрограммы

• Возобновление работы сопрограммы происходит с использованием планировщика:

struct Journey {

void proceed() {

scheduler->schedule([this] {

coro.resume();

onComplete();

});

}

void onComplete(); // вызов action в defer

// или удаление сопрограммы

private:

IScheduler* scheduler;

Coro coro;

};

Page 31: Асинхронность и сопрограммы

Телепортация

• Переключение между планировщиками. Внутри сопрограммы:

ThreadPool tp1(1);

ThreadPool tp2(1);

go([&tp2] {

JLOG("Inside tp1");

teleport(tp2);

JLOG("And now inside tp2!!!");

}, tp1);

• Реализация:

void Journey::teleport(IScheduler& s) {

scheduler = &s;

defer(proceedAction());

}

Page 32: Асинхронность и сопрограммы

Телепортация

Происходит переключение исполнения сопрограммы с Scheduler/Thread на Scheduler2/Thread2

Page 33: Асинхронность и сопрограммы

Портал

• Портал - телепортация туда и обратно:

struct Portal {

Portal(IScheduler& destination)

: source(currentScheduler()) {

teleport(destination);

}

~Portal() {

teleport(source);

}

private:

IScheduler& source;

};

Page 34: Асинхронность и сопрограммы

Использования портала

• Пример использования портала:

struct Network {

void handleNetworkEvents() {

// долгие и нетривиальные операции

}

};

ThreadPool common(1);

ThreadPool networkPool(1);

portal<Network>().attach(networkPool);

go([] {

JLOG("Execution inside common");

portal<Network>()->handleNetworkEvents();

JLOG("Continue execution inside common");

}, common);

Page 35: Асинхронность и сопрограммы

Alone

• Для глобальных объектов часто требуется эксклюзивный доступ. Обычно используется мьютексы.

• Сопрограммы позволяют ввести новый паттерн: Alone.

struct Alone : IScheduler {

void schedule(Action action) {

strand.post(std::move(action));

}

private:

boost::asio::io_service::strand strand;

};

• Гарантирует выполнение не более одного action в каждый момент времени.

Page 36: Асинхронность и сопрограммы

Интегральный пример

struct DiskCache/MemCache {

boost::optional<std::string> get(const std::string& key);

void set(const std::string& key, const std::string& val);

};

ThreadPool commonPool(3); // общий пул потоков

ThreadPool diskPool(2); // пул для дисковых операций

Alone memAlone(commonPool); // сериализация действий с памятью

portal<DiskCache>().attach(diskPool); // привязка портала для диска

portal< MemCache>().attach(memAlone); // привязка портала для памяти

boost::optional<std::string> result = goAnyResult<std::string>({

[&key] { return portal<DiskCache>()->get(key); },

[&key] { return portal< MemCache>()->get(key); }

});

Page 37: Асинхронность и сопрограммы

Обсуждение использования порталов

struct MemCache {

boost::optional<std::string> get(const std::string& key);

void set(const std::string& key, const std::string& val);

};

portal<MemCache>()->set(key, val);

Дружественность к исключениям

Абстракция от контекста исполнения: возможно использование с различными планировщиками.

Порталы – аналог акторов:

– Типы сообщений → методы

– Сообщение → параметры метода

Page 38: Асинхронность и сопрограммы

План рассказа

Реализация сопрограмм

Ожидающие примитивы

Примитивы, использующие планировщик

Заключение

Page 39: Асинхронность и сопрограммы

Теорема

Любую асинхронную задачу можно решить с помощью сопрограмм

1. После вызова код отсутствует:

// код до вызоваasync(..., action);

// код до вызоваsynca(...); action();

2. После вызова код присутствует:

// код до вызоваasync(..., action);// код после вызова

// код до вызоваgo { async(..., action); }// код после вызова

// код до вызоваgo { synca(...); action(); }// код после вызова

Page 40: Асинхронность и сопрограммы

Бонус: простой GC

struct A { ~A() { TLOG("~A"); } };

struct B:A { ~B() { TLOG("~B"); } };

struct C { ~C() { TLOG("~C"); } };

go([] {

A* a = gcnew<B>();

C* c = gcnew<C>();

});

Обратите внимание на невиртуальные деструкторы!

Вывод в консоли:

tp#1: [1] endedtp#1: ~Ctp#1: ~Btp#1: ~A

Page 41: Асинхронность и сопрограммы

Реализация простого GC

template<typename T, typename... V>T* gcnew(V&&... v) {

return gc().add(new T(std::forward(v)...));}GC& gc() { return journey().gc; }

struct GC {

~GC() {

for (auto& deleter: boost::adaptors::reverse(deleters))

deleter();

}

template<typename T> T* add(T* t) {

deleters.emplace_back([t] { delete t; });

return t;

}

private:

std::vector<Handler> deleters;

};

Page 42: Асинхронность и сопрограммы

Выводы

• Простота синхронного подхода

• Эффективность асинхронного подхода

• Бережное использование ресурсов (нет ожиданий)

• Эффективная реализация многопоточных паттернов

• Введение новых паттернов (телепортация, порталы, …)

• Это прикольно!

Код: https://bitbucket.org/gridem/synca

[email protected]

http://habrahabr.ru/users/gridem

Page 43: Асинхронность и сопрограммы

Спасибо за внимание!