Выпуск 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 →