Transparent coroutines

в 20:14, , рубрики: c++, coroutines, highload, высокая производительность, Программирование, Разработка под Linux

Когда мне становится грустно, я пишу ни кому не нужные библиотеки...

В интернете полно статей про сопрограммы (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 На данный момент в библиотеке не реализована поддержка таймаутов и дискового ввода/вывода. Как станет грустно — обязательно добавлю.

Написание патчей приветствуется!

Автор: Юрий Дьяченко

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js