Когда мне становится грустно, я пишу ни кому не нужные библиотеки...
В интернете полно статей про сопрограммы (coroutines) и хабр эта тема не обошла стороной. Вот например, замечательные статьи: Использование Boost.Asio с Coroutines TS, Основы Userver — фреймворка для написания асинхронных микросервисов, но все это становится бесполезно, когда в сопрограмме вам необходимо вызвать функцию из библиотеки, которая делает блокирующий ввод/вывод.
Встречайте еще одну ни кому ненужную библиотеку: yurco — библиотека, которая поможет встроить сопрограммы прозрачно для стороннего кода.
Для начала рассмотрим фрагмент кода из примера к библиотеке:
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
Q: Что за unistd:*
?
A: Это другая ни кому не нужная библиотека. Она реализует простые обертки над системным функциями. Они проверяют код возврата и в случае ошибки кидают 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 или другой библиотеки. Если быть совсем точным, то функция ::swapcontext
была переписана — из нее был убран вызов rt_sigprocmask, в остальном код остался неизменным.
Основные классы библиотеки: Reactor и Coroutine. Reactor позволяет создать экземпляр класса для запуска новой сопрограммы, усыпить сопрограмму и разбудить ее при доступности не блокирующего ввода/вывода. Ряд системный функций для ввода/вывода имеет обертки, автоматически приостанавливающие выполнение сопрограммы при ожидании события на файловом дескрипторе. Как видно, эти обертки требуют передачи экземпляра Reactor и Coroutine при вызове. Эту проблему можно решить используя специальные функции из библиотеки pthreads: pthread_getspecific()
, pthread_setspecific()
. При вызове Reactor::run()
будет автоматичеки вызвана функция pthread_setspecific(this)
, позволяя в будущем получить текущий экземпляр реактора в любом месте. Тоже самое сделано и для сопрограммы: когда сопрограмма запускается или продолжается ее выполнение, вызывается pthread_setspecific(this)
, позволяя получить доступ к текущему экземпляру класса Coroutine. Остается только создать функции с таким же прототипом как и системные вызовы ввода/вывода, но приостанавливающие сопрограмму при ожидании ввода/вывода.
Последний механизм использующийся в реализации магии это подмена вызова системных функций ввода/вывода на наши обертки. Это можно реализовать с помощью LD_PRELOAD и динамически подгружаемой библиотеки (не реализовано на момент написания статьи) или с помощью специальной опции линкера -wrap
при статической линковке. Вот полный пример используемый в статье (имеется CMakeList.txt).
ps На данный момент в библиотеке не реализована поддержка таймаутов и дискового ввода/вывода. Как станет грустно — обязательно добавлю.
Написание патчей приветствуется!
Автор: Юрий Дьяченко