- PVSM.RU - https://www.pvsm.ru -
Когда мне становится грустно, я пишу ни кому не нужные библиотеки...
В интернете полно статей про сопрограммы (coroutines) и хабр эта тема не обошла стороной. Вот например, замечательные статьи: Использование Boost.Asio с Coroutines TS [1], Основы Userver — фреймворка для написания асинхронных микросервисов [2], но все это становится бесполезно, когда в сопрограмме вам необходимо вызвать функцию из библиотеки, которая делает блокирующий ввод/вывод.
Встречайте еще одну ни кому ненужную библиотеку: yurco [3] — библиотека, которая поможет встроить сопрограммы прозрачно для стороннего кода.
Для начала рассмотрим фрагмент кода из примера к библиотеке:
void process_connection(unistd::fd& fd)
{
try
{
char buf[100];
unistd::read(fd, buf, sizeof(buf));
/* Create a connection */
std::unique_ptr<MYSQL, std::function<decltype(mysql_close)>> con(mysql_init(nullptr), mysql_close);
mysql_real_connect(con.get(), "db.local", "ro", "", nullptr, 0, nullptr, 0);
mysql_query(con.get(), "SELECT NOW(), SLEEP(10);");
std::unique_ptr<MYSQL_RES, std::function<decltype(mysql_free_result)>> result(mysql_store_result(con.get()), mysql_free_result);
if (!result)
return; // silently close connection
for (MYSQL_ROW row = mysql_fetch_row(result.get()); row; row = mysql_fetch_row(result.get()))
{
static char header[] = "HTTP/1.1 200 OKrnContent-Length: 21rnConnection: closernrn";
unistd::write_all(fd, header, strlen(header));
const char* const answer = row[0];
unistd::write_all(fd, answer, strlen(answer));
unistd::write_all(fd, "rn", 2);
}
}
...
Исходный код https://github.com/yurial/yurco/blob/master/examples/mysql.cpp [4]
Q: Что за unistd:*
?
A: Это другая [5] ни кому не нужная библиотека. Она реализует простые обертки над системным функциями. Они проверяют код возврата и в случае ошибки кидают std::system_error
. Ни какой магии тут нет, просто код становится чуть лаконичнее.
Q: А unistd::fd
?
A: Простенький класс, который делает ::close()
в деструкторе и ::dup()
при копировании. Тоже ни какой магии.
Q: А для чего SLEEP(10)
в SQL запросе?
A: Это сделано специально, программа работающая с блокирующим вводом/выводом подвиснет здесь на 10 секунд и не будет обрабатывать другие запросы.
Q: А почему код так неуклюже работает с mysql, HTTP, etc?
A: Правильность кода в данном примере не важна, это увеличит объем и затруднит понимание главного:
Может показаться, что данная функция работает синхронно, делая блокирующий ввод/вывод (как минимум при выполнении SQL запроса, ведь libmysqlclient ни чего не знает про сопрограммы), но на самом деле все не так. Благодаря магии и какой-то там матери этот код выполняется в сопрограмме, бережно прерываясь на операциях ввода/вывода.
В основе данной библиотеки лежит ::swapcontext
, хотя того же эффекта можно достичь и используя boost.coroutine [6] или другой библиотеки. Если быть совсем точным, то функция ::swapcontext
была переписана — из нее был убран вызов rt_sigprocmask, в остальном код остался неизменным.
Основные классы библиотеки: Reactor и Coroutine. Reactor позволяет создать экземпляр класса для запуска новой сопрограммы, усыпить сопрограмму и разбудить ее при доступности не блокирующего ввода/вывода. Ряд системный функций для ввода/вывода имеет обертки [7], автоматически приостанавливающие выполнение сопрограммы при ожидании события на файловом дескрипторе. Как видно, эти обертки требуют передачи экземпляра Reactor и Coroutine при вызове. Эту проблему можно решить используя специальные функции из библиотеки pthreads: pthread_getspecific()
, pthread_setspecific()
. При вызове Reactor::run()
будет автоматичеки вызвана функция pthread_setspecific(this)
, позволяя в будущем получить текущий экземпляр реактора в любом месте. Тоже самое сделано и для сопрограммы: когда сопрограмма запускается или продолжается ее выполнение, вызывается pthread_setspecific(this)
, позволяя получить доступ к текущему экземпляру класса Coroutine. Остается только создать функции [8] с таким же прототипом как и системные вызовы ввода/вывода, но приостанавливающие сопрограмму при ожидании ввода/вывода.
Последний механизм использующийся в реализации магии это подмена вызова системных функций ввода/вывода на наши обертки. Это можно реализовать с помощью LD_PRELOAD и динамически подгружаемой библиотеки (не реализовано на момент написания статьи) или с помощью специальной опции линкера -wrap
при статической линковке. Вот [9] полный пример используемый в статье (имеется CMakeList.txt).
ps На данный момент в библиотеке не реализована поддержка таймаутов и дискового ввода/вывода. Как станет грустно — обязательно добавлю.
Написание патчей приветствуется!
Автор: Юрий Дьяченко
Источник [10]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/programmirovanie/353770
Ссылки в тексте:
[1] Использование Boost.Asio с Coroutines TS: https://habr.com/ru/post/348602/
[2] Основы Userver — фреймворка для написания асинхронных микросервисов: https://habr.com/ru/company/yandex/blog/474438/
[3] yurco: https://github.com/yurial/yurco
[4] https://github.com/yurial/yurco/blob/master/examples/mysql.cpp: https://github.com/yurial/yurco/blob/master/examples/mysql.cpp
[5] другая: https://github.com/yurial/unistd
[6] boost.coroutine: https://www.boost.org/doc/libs/1_53_0/libs/coroutine/doc/html/coroutine/intro.html
[7] обертки: https://github.com/yurial/yurco/blob/master/operations.hpp
[8] функции: https://github.com/yurial/yurco/blob/master/transparency.hpp
[9] Вот: https://github.com/yurial/yurco-dev/tree/bf651f60b854e2eebce0a717840ccdb18e23afbb
[10] Источник: https://habr.com/ru/post/505642/?utm_source=habrahabr&utm_medium=rss&utm_campaign=505642
Нажмите здесь для печати.