Выпуск 1. Март 2013

Padre IDE. В шаге от релиза 1.0 | Содержание | Что нового в Perl 5.17.9

Всё, что вы хотели знать об AnyEvent, но боялись спросить

AnyEvent на сегодняшний день является самым популярным современным фреймворком событийно-ориентированного программирования (СОП) в Perl. Об этом, в частности, свидетельствует 25-ая позиция в Top-100 рейтинге модулей MetaCPAN.

Нужно отметить, что парадигма СОП является наиболее оптимальным выбором для программирования сетевых приложений в Perl, показывая значительно лучшую масштабируемость, чем использование форков или тредов. Треды в Perl имеют особый статус, но как хорошо заметил Алан Кокс:

Компьютер — это конечный автомат. Треды для тех людей, которые не умеют программировать конечные автоматы.

Тем не менее, в современных сетевых приложениях замечается тенденция к комбинированию подходов. Например, использовать prefork — предварительный запуск нескольких рабочих процессов, каждый из которых имеет свой главный цикл обработки событий.

AnyEvent представляет собой слой абстракции над какой-либо из реализаций СОП. В этом отношении он похож на модуль DBI, который абстрагируется от различных API баз данных, предоставляя единый интерфейс для всех.

Существует достаточно много модулей реализующих событийное программирование: EV, Event, Glib, Tk, Lib::Event, Irssi, IO::Async, Qt, FLTK и POE. Как правило, если в программе начинается использовать одна из реализацией СОП, вы больше не можете начать использовать другую в рамках того же процесса. Это порождает зависимость на выбранный фреймворк и принуждает всех, кто будет использовать ваш модуль также использовать только этот фреймворк. В сложных проектах, составленных из множества компонент это приводит к тому, что для включения какого-либо модуля его приходиться править под используемую реализацию СОП, а если какой-то функционал выбранного вами фреймворка является уникальным, то возможно придётся переписывать всю логику модуля с нуля.

AnyEvent был призван устранить эту проблему путём создания абстрактного интерфейса для программиста для работы с событиями. Таким образом, код перестаёт зависеть от конкретной реализации СОП, позволяя использовать модуль в работе с любой реализацией, выбранной пользователем или доступной в момент запуска. В состав AnyEvent включены адаптеры для большинства популярных реализаций: Cocoa::EventLoop, Lib::Event, Event, EV, FLTK, Glib, IO::Async, Irssi, POE, Qt, Tk. Кроме того, в AnyEvent включена простая реализация AnyEvent::Loop, подключаемая в том случае, если ничего другого не было найдено. Это бывает удобно, если ваша программа не должна иметь XS-зависимостей.

Интерфейс AnyEvent

Рассмотрим интерфейс AnyEvent. Основной сущностью, над которой мы оперируем в программе является страж (или наблюдатель) — объект, который хранит относящиеся к наблюдаемым событиям данные, как например, функцию-колбэк, файловый дескриптор и т.д. Существует несколько типов стражей.

  • страж ввода/вывода для файловых дескрипторов:

    $w = AnyEvent->io (
        fh   => <filehandle_or_fileno>,
        poll => <"r" or "w">,
        cb   => <callback>,
    );
  • страж времени (таймер) для отслеживания временных интервалов:

    $w = AnyEvent->timer (
        after    => <fractional_seconds>
        interval => <fractional_seconds>,
        cb => <callback>,
    );
  • страж сигналов для отслеживания сигналов, получаемых процессом:

    $w = AnyEvent->signal (
        signal => <uppercase_signal_name>,
        cb => <callback>,
    );
  • страж процессов потомков. Данный тип был выделен, чтобы иметь возможность обрабатывать каждого потомка отдельно. С предыдущим типом это делать затруднительно, так как сигнал SIGCHLD не может быть распределён на нескольких обработчиков:

    $w = AnyEvent->child (
        pid => <process_id>,
        cb => <callback>,
    );
  • страж простоя для выполнения задач, когда в очереди нет никаких других событий для обработки:

    $w = AnyEvent->idle (
        cb => <callback>
    );
  • переменные состояния — это уникальный тип, который можно отождествить с обещаниями, т.е. будущими значениями, которые мы ожидаем получить. Поскольку в интерфейсе AnyEvent нет как такового основного цикла событий (т.е. когда программа переходит в режим блокирующего ожидания), то единственным способ заблокироваться в ожидании — попытаться получить значение переменной состояния:

    $cv = AnyEvent->condvar;
    
    $cv->send (<list_of_variables>);
    my @res = $cv->recv;

Одна из интересных особенностей интерфейса AnyEvent — это необходимость использования замыканий, поскольку нет другой возможности передать параметры в колбэк-функцию. С одной стороны это смущает новичков, но с другой стороны это имеет свои плюсы, поскольку замыкания работают быстрее и используют чуточку меньше ресурсов, т.к. не требуют создания локальных переменных для передачи параметров в функцию.

Для понимания рассмотрим небольшой пример. Программа ожидает ввода пользователя с STDIN, и если ввода нет в течении 4.2 секунд — прерывает свою работу. Если ввод есть — выводит введённую строку.

my $done = AnyEvent->condvar;

my ($w, $t);

$w = AnyEvent->io (
    fh => \*STDIN,
    poll => 'r',
    cb => sub {
        chomp (my $input = <STDIN>);
        warn "read: $input\n";
        undef $w;
        undef $t;
        $done->send();
    });

$t = AnyEvent->timer (
    after => 4.2,
    cb => sub {
        if (defined $w) {
            warn "no input for a 4.2 sec\n";
            undef $w;
            undef $t;
        }
        $done->send();
    });

$done->recv()

Чтобы переменные-стражи $done, $w, $t попадали в замыкания функций-колбэков, они должны быть в их области видимости. По этой причине мы сначала объявляем переменные через ключевое слово my и только после этого присваиваем значение. Как видно, страж (таймер или страж файлового дескриптора) можно удалить как и любой объект в Perl присвоив ему, например, неопределённое значение. Вызов $done->recv() приводит к блокирующему ожиданию, пока в одном из колбэков не будет сделан вызов $done->send().

Модули дистрибутива AnyEvent

Собственно всей этой нехитрой азбуки стражей AnyEvent достаточно для реализации задач широкого спектра сложности. Но на практике всё же бывает удобно использовать более высокоуровневый функционал, чтобы не изобретать велосипед каждый раз при написании проектов.

  • AnyEvent::Handle — это эволюционный потомок стража ввода/вывода AnyEvent->io, значительно облегчающий работу с потоковыми сокетами. Поддерживаются очереди чтения и записи, встроенная сериализация/десериализация JSON структур, поддержка TLS и т.п.;

  • AnyEvent::Socket — специальный модуль для высокоуровневой работы с сокетами (tcp или unix), который предоставляет нам несколько полезных функций, в том числе, tcp_server и tcp_client, назначение которых ясно из названия. Поддерживается ipv4, ipv6, а также unix-сокеты. Возможно использование SSL/TLS;

  • AnyEvent::Log — небольшой фреймворк для ведения логов, позволяющий удобно отлаживать приложения, написанные на AnyEvent;

  • AnyEvent::Util — чрезвычайно полезный набор функций, многие из которых являются асинхронными аналогами библиотечных функций:
    • portable_pipe — аналог pipe, который работает даже в Windows;
    • portable_socketpair — pipe для двустороннего обмена;
    • fork_call — асинхронный вызов кода в отдельном процессе и передача возвращаемых данных (через Storable) обратно в материнский процесс в указанный колбэк;
    • run_cmd — аналог команды system, позволяющий запускать программы и асинхронно обрабатывать выводы STDOUT, STDERR, ввод на STDIN и другие открытые файловые дескрипторы.
  • AnyEvent::DNS — полностью асинхронная реализация разрешения DNS-имён;

  • AnyEvent::IO — интерфейс для асинхронной работы с файлами. На данный момент этот модуль поддерживает только один бэкенд — IO::AIO, который позволяет выполнять неблокирующиеся файловые операции. Как известно Linux не поддерживает неблокирующиеся операции над файлами при использовании стандартных системных вызовов. Именно эту проблему и пытается решить IO::AIO. Для этого он запускает подобные вызовы в отдельном треде (при этом не имеет значения собран ли Perl с поддержкой тредов — это внутренняя кухня модуля).

AnyEvent на CPAN

На CPAN появляются всё больше и больше расширений для AnyEvent. Перечислять всех смысла нет, можно отметить лишь некоторые интересные.

  • AnyEvent::HTTP — модуль является своеобразным асинхронным LWP, выполняя функции http/https клиента. Есть поддержка прокси, keep-alive соединений и сессий, всех HTTP методов и обработки cookie. Не реализована поддержка HTTP-авторизации, но думаю, что это не за горами;

  • AnyEvent::DBI — модуль для асинхронной работы с СУБД;

  • Twiggy — один из самых известных и популярных веб-серверов использующих AnyEvent.

Подводные камни AnyEvent

Начав работать с AnyEvent надо всегда помнить о контексте, в котором вызываются функции модулей AnyEvent. Многие функции в скалярном контексте возвращают, так называемый охранный объект. Как только охранный объект уничтожается (например, выход из области видимости), вызываемая функция будет автоматически отменена. При этом если данную функцию вызвать в пустом контексте, охранный объект не будет создаваться и прервать его у вас уже возможности не будет. Так ведёт себя, например, функция tcp_connect из модуля AnyEvent::Socket.

Создав переменную состояния, никогда не запускайте метод recv(), если знаете, что в коде нигде не будет вызываться метод send(). Разные реализации СОП ведут себя по-разному в подобной ситуации. EV, например, просто начинает загружать процессор на 100%.

Нельзя блокироваться в вызываемых функциях-колбэках. Как только вы попытаетесь вызвать метод recv() внутри колбэка, AnyEvent обнаружит эту ситуацию и сообщит об ошибке.

Кроме того нельзя использовать die() внутри колбэка, поскольку нормально обработать это событие будет затруднительно. Впрочем в EV, будет выдано предупреждение о смерти колбэка, но программа при этом продолжит выполнение.

Не следует менять значение глобальных переменных (таких как $_ или $[) внутри колбэков.

В большинстве СОП библиотек небезопасно использовать fork. Это означает, что выполнив fork вы не можете начать обрабатывать события в процессе потомке, если до этого в материнском процессе уже была запущена обработка событий. Единственным исключением является библиотека EV. Но даже в этом случае процесс потомок получает клоны всех таймеров и стражей I/O, которые начинают работать в обоих процессах, что может быть не совсем то, что вы хотите. Поэтому лучше использовать fork до запуска обработки событий или не использовать в дочернем процессе никаких функций AnyEvent. Также можно запускать рабочий процесс потомка с помощью комбинации fork+exec.

Бенчмарки

Документация AnyEvent содержит любопытные результаты бенчмарков по оценке производительности AnyEvent с использованием различных бэкендов. В бенчмарке создаётся большое количество I/O стражей, оценивается время создания, уничтожения стражей, потребляемая память на объект и время вызова колбэка.

        name watchers bytes create invoke destroy
       EV/EV   100000   223   0.47   0.43    0.27
      EV/Any   100000   223   0.48   0.42    0.26
Coro::EV/Any   100000   223   0.47   0.42    0.26
    Perl/Any   100000   431   2.70   0.74    0.92
 Event/Event    16000   516  31.16  31.84    0.82
   Event/Any    16000  1203  42.61  34.79    1.80
 IOAsync/Any    16000  1911  41.92  27.45   16.81
 IOAsync/Any    16000  1726  40.69  26.37   15.25
    Glib/Any    16000  1118  89.00  12.57   51.17
      Tk/Any     2000  1346  20.96  10.75    8.00
     POE/Any     2000  6951 108.97 795.32   14.24
     POE/Any     2000  6648  94.79 774.40  575.51

Результаты подробно поясняются в документации. Как видно, безусловным лидером является EV, а аутсайдером — POE.

Заключение

Возможно, AnyEvent сэкономит вам кучу времени и сил, благодаря своему обширному спектру возможностей. Возможно, у вас появилась идея какой модуль можно написать, чтобы пополнить коллекцию приложений AnyEvent. В любом случае знакомство с AnyEvent будет полезным и интересным опытом.

Владимир Леттиев


Padre IDE. В шаге от релиза 1.0 | Содержание | Что нового в Perl 5.17.9
Нас уже 1393. Больше подписчиков — лучше выпуски!

Комментарии к статье