Как показывает практика, повсеместное применение классического, основанного на callback’ах подхода к асинхронному программированию обычно оказывается неудобным. Для упрощения написания и поддержки сложного асинхронного кода можно использовать иной подход — с использованием сопрограмм. Он значительно сокращает объём и сложность кода, превращая код из асинхронного в синхронный.
5. Многопоточный сервер
Acceptor acceptor(80);
while (true) {
socket = acceptor.accept();
go([socket] { // запуск в новом потоке
request = socket.read();
response = handleRequest(request);
socket.write(response);
});
}
• Недостатки:
o Большую часть времени потоки ожидают на I/O событиях и не
делают полезной работы => бесполезная трата ресурсов
7. Асинхронность
Когда заканчивается операция?
1. Мы вызываем: периодический опрос состояния - реакторная
модель (select, epoll, kqueue)
2. Нас вызывают: проакторная модель исполнения (UI, IOCP,
javascript):
void onActionComplete(Result) {...}
action1(onActionComplete);
Будем использовать проактор boost.asio
9. Асинхронный сервер: обсуждение
• Достоинства
o Производительность
o Относительно простая параллелизация
• Недостатки
o Сложность:
Передача контекста
Нелинейная структура кода
Обработка ошибок
Время жизни объектов
Отладка
С ростом количества операций сложность растет нелинейно
10. Решение
Что бы хотелось?
• У асинхронного взять быстродействие
• У синхронного - простоту
Т.е. объединить достоинства и убрать недостатки, присущие обоим
методам.
Как этого добиться? Ответ: использовать сопрограммы!!!
11. Сопрограммы
• Подпрограмма: выполняются все действия перед возвратом
управления
• Сопрограмма: частичное выполнение действий перед возвратом
управления
Сопрограмма обобщает понятие подпрограммы, т.е. подпрограмма -
это частный случай сопрограммы.
Пример сопрограммы: генераторы (например, на python)
Для эффективной реализации на С++ будем использовать метод
сохранения контекста исполнения (stackful coroutines).
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
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)
17. Проблема: состояние гонки
Возможно возобновление
работы сопрограммы до
выхода из нее
Варианты решения:
1. std::mutex
2. boost::asio::strand
3. defer
18. Решение: defer
defer: откладывание асинхронной
операции до выхода из
сопрограммы
typedef
std::function<void()> Action;
void defer(Action);
22. Ожидание завершения операции
• Задача: запустить асинхронно операцию и дождаться её завершения.
void goWait(Action action);
• Проблема обычного подхода: во время ожидания поток остается
заблокированным
• Сопрограммы с легкостью решают эту проблему
24. Ожидание завершения операций
• Задача: запустить асинхронно несколько операций и дождаться их
завершения.
void goWait(std::initializer_list<Action> actions);
• Проблема: во время ожидания поток остается заблокированным
• Сопрограммы с легкостью решают эту проблему
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);
35. Alone
• Для глобальных объектов часто требуется эксклюзивный доступ.
Обычно используется мьютексы.
• Сопрограммы позволяют ввести новый паттерн: Alone.
struct Alone : IScheduler {
void schedule(Action action) {
strand.post(std::move(action));
}
private:
boost::asio::io_service::strand strand;
};
• Гарантирует выполнение не более одного action в каждый момент
времени.
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); }
});
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);
Дружественность к исключениям
Абстракция от контекста исполнения: возможно использование с
различными планировщиками.
Порталы – аналог акторов:
– Типы сообщений → методы
– Сообщение → параметры метода
39. Теорема
Любую асинхронную задачу можно решить с помощью сопрограмм
1. После вызова код отсутствует:
// код до вызова
async(..., action);
// код до вызова
synca(...); action();
2. После вызова код присутствует:
// код до вызова
async(..., action);
// код после вызова
// код до вызова
go { async(..., action); }
// код после вызова
// код до вызова
go { synca(...); action(); }
// код после вызова
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] ended
tp#1: ~C
tp#1: ~B
tp#1: ~A
не показано:
время жизни
обработка ошибок
проблема с зацикливанием
-> создается прокси-класс Portal
с исключениями тоже работает!
-- отличие: пассивность: исполняет вызывающий, а не вызываемый! Аналог - лопата и тот, кто копает, можно делать параллельно, что в случае актора - достаточно затруднительно
легко прикручивается аллокация из пулов памяти!!!
легко прикручивается аллокация из пулов памяти!!!
прикольно: расширение языка для решения своих задач
иной подход и взгляд на вещи, расширение кругозора
всякие бонусы