Выпуск 11. Январь 2014
← Про опыт разработки на Perl под Raspberry PI | Содержание | Обход дерева директорий на Perl и Haskell (часть 1) →Асинхронное программирование с IO::Async
Наряду с популярным AnyEvent для Perl существуют другие модули для событийно-ориентированного программирования (СОП), и, в том числе, модуль IO::Async, автором которого является Paul Evans. Рассмотрим чем же интересен данный швейцарский нож и какими уникальными лезвиями он обладает.
IO::Async
IO::Async содержит обширный набор модулей, которые позволяют решать разные небольшие задачи, такие как отслеживание состояния дескрипторов ввода/вывода (возможность записать или прочесть данные), запуск таймеров, перехват сигналов, управление процессами-потомками, наблюдение за изменениями в файловой системе и даже управление потоками выполнения (сопрограммы).
Важная особенность IO::Async — это активное использование так называемых обещаний ( Promises ), которые в терминологии IO::Async именуются объектам Future из одноимённого модуля. Данный объект представляет собой некоторую выполняемую задачу, которая ещё не завершилась. Future появились в IO::Async не так давно и ещё активно прорабатывается, но тенденция к повсеместному их использованию чётко прослеживается.
IO::Async может оказаться особенно удобным при использовании ООП подхода при разработке приложений, поскольку позволяет наследовать свои базовые классы и задавать обработку событий в методах ваших модулей.
IO::Async::Loop
IO::Async::Loop — это базовый модуль, который задаёт цикл обработки событий в программе. Типичная создаваемая программа создаёт один цикл и добавляет в него обработчики различных событий, после чего запускает цикл в работу.
use IO::Async::Loop;
# Создание цикла
my $loop = IO::Async::Loop->new;
# Добавляем какой-либо обработчик
$loop->add(...);
# Запускаем главный цикл обработки событий
$loop->run
Класс IO::Async::Loop является абстрактным и под его капотом может работать какая-либо из существующих реализаций мультиплексора событий, например, идущие в поставке IO::Async::Loop::Select и IO::Async::Loop::Poll, соответственно использующие системные вызовы select и poll. На CPAN также могут быть найдены реализации с использованием специфический системных вызовов, например, kqueue и epoll, или библиотек EV и UV.
Приоритетом выбора того или иного бэкенда можно управлять, например, через переменную окружения IO_ASYNC_LOOP или переменную $IO::Async::Loop::LOOP, в которых задаётся разделённая запятой последовательность субклассов:
$IO::Async::Loop::LOOP = 'EV,Epoll,Poll'
Или непосредственно создав цикл из нужного субкласса:
use IO::Async::Loop::Poll;
my $loop = IO::Async::Loop::Poll->new;
Уведомители IO::Async::Notifier
Для каждого наблюдаемого события создаётся свой так называемый уведомитель — объект базового класса IO::Async::Notifier, который и выполняет все низкоуровневые операции по наблюдению.
IO::Async::Handle— наблюдение за событиями ввода/вывода файлового дескриптораIO::Async::Stream— подкласс для работы с потоковыми протоколами обменаIO::Async::Socket— подкласс для работы с датаграммами или сокетом напрямуюIO::Async::Timer— базовый класс для таймеровIO::Async::Timer::Absolute— вызов процедуры в заданное времяIO::Async::Timer::Countdown— вызов процедуры через заданный промежуток времениIO::Async::Timer::Countdown— периодический вызов процедурыIO::Async::Signal— наблюдение и обработка получаемых процессом сигналовIO::Async::Process— запуск и наблюдение за дочерними процессамиIO::Async::PID— отслеживание завершения дочернего процессаIO::Async::File— отслеживание изменений файла (поля в выводеstat())IO::Async::FunctionиIO::Async::Routine— асинхронный вызов функции (вызов кода в дочернем процессе/треде)
Операции с вводом/выводом
На примере создания сетевого сервера, рассмотрим использование IO::Async::Handle и IO::Async::Stream. Сервер будет принимать соединения на порт 1234, читать ввод построчно и возвращать перевёрнутую строку в ответ:
use IO::Socket::INET;
use IO::Async::Handle;
use IO::Async::Stream;
use IO::Async::Loop;
my $loop = IO::Async::Loop->new;
# Создание сокета, ожидающего соединения на порт 1234
my $socket = IO::Socket::INET->new( LocalPort => 1234, Listen => 1, ReuseAddr=>1 );
# Создание хендла для наблюдения за сокетом
my $handle = IO::Async::Handle->new(
handle => $socket,
on_read_ready => sub {
# Новое подключение. Вызов accept() на сокете
my $client = $socket->accept or die $!;
# Создание потокового сервера
my $stream; $stream = IO::Async::Stream->new(
handle => $client,
close_on_read_eof => 0,
# Читаем полученные данные
on_read => sub {
my ( $self, $buffref, $eof ) = @_;
# Читаем данные из буфера построчно
# Пишем данные ответа в выходной буфер потока
while( $$buffref =~ s/^(.*)\n// ) {
$stream->write(scalar reverse "\n".$1);
}
# Если получен EOF, то закрываем соединение,
# как только все данные будут отправлены
$stream->close_when_empty() if $eof;
# не вызывать повторно эту функцию после EOF
return 0;
},
);
# Добавляем поток в цикл
$loop->add( $stream );
},
);
# Добавление хендла в цикл
$loop->add( $handle );
# Запуск цикла
$loop->run
Каждое новое соединение обрабатывается с помощью класса IO::Async::Stream, который оптимизирован для работы с потоковыми данными и автоматически формирует буфер для чтения и записи, которые передаются в виде ссылки в соответствующую функцию-колбек.
Операции ввода/вывода и объекты Future
Создание сетевого сервера и клиента достаточно распространённая задача, поэтому данный функционал был добавлен в IO::Async::Loop. Предыдущий пример можно переписать следующим образом:
use IO::Async::Loop;
my $loop = IO::Async::Loop->new;
# Создание сокета, прослушивающего порт 1234
$loop->listen(
family => "inet",
socktype => "stream",
service => 1234,
on_resolve_error => sub { die "Cannot resolve - $_[0]\n"; },
on_listen_error => sub { die "Cannot listen\n"; },
# Обработка подключений
on_stream => sub {
my $stream = shift;
# Конфигурируем поток
$stream->configure( on_read => sub { 0 }, close_on_read_eof => 0 );
# Добавляем поток в цикл
$loop->add( $stream );
# Возвращается Future-объект для операции чтения потока до EOF
my $f = $stream->read_until_eof;
# Функция-колбек при завершении операции
$f->on_done(sub {
my ($buf, $eof) = @_;
while( $buf =~ s/^(.*)\n// ) {
$stream->write(scalar reverse "\n".$1);
}
$stream->close_when_empty() if $eof;
return 0;
});
}
);
$loop->run
В данном примере при обработке клиентских подключений автоматически выполняется accept() и в функцию-колбэк on_stream передаётся готовый объект IO::Async::Stream. С помощью метода configure существует возможность сконфигурировать поток точно также, как это было сделано в первом примере. Но в данном случае продемонстрирован новый подход по работе с асинхронными функциями: использование Future-объектов.
Метод read_until_eof() возврашает объект Future — обещание, т.е. некоторое задание, которое находится в процессе выполнения. Само задание — вычитать поток до EOF. С помощью метода on_done мы устанавливаем функцию, которая будет вызвана, когда задание будет успешно выполнено.
С первого взгляда не видно никаких преимуществ использования Future-объектов. Попробуем усложнить задачу сервера. Теперь на каждую строку, начинающуюся с http:// выполнять загрузку указанного url и возвращать клиенту его содержимое.
Без использования Future-объектов это можно реализовать так:
sub on_read {
my ( $self, $buffref, $eof ) = @_;
# Учёт количества запросов
my $req = 0;
while( $$buffref =~ s/^(.*)\n// ) {
my $url = $1;
if ($url =~ m{^http\://} ) {
$req++;
# http-запрос
my $http = Net::Async::HTTP->new();
$loop->add( $http );
$http->do_request(
uri => $url,
on_response => sub {
my ($response) = @_;
$stream->write($response->content);
$req--;
},
on_error => sub {
$req--;
}
)
}
}
if ($eof and $req == 0) {
$stream->close_when_empty();
}
return 0;
}
Как видно мы получаем вложенную функцию-колбек on_response, которая будет собирать результаты http-запросов и отправлять клиенту их результат. Если бы нам потребовалось выполнить ещё несколько асинхронных действий после успешного http-запроса, это привело бы к дополнительным вложенным колбекам, что усложняло бы восприятие кода.
Рассмотрим вариант с Future-объектами:
sub on_stream {
my $stream = shift;
$stream->configure( on_read => sub { 0 }, close_on_read_eof => 0 );
$loop->add( $stream );
my $f = $stream
->read_until_eof
->then(sub {
my ($buf, $eof) = @_;
my @req = ();
while( $buf =~ s/^(.*)\n// ) {
my $url = $1;
next if $url !~ m{http://};
my $http = Net::Async::HTTP->new();
$loop->add( $http );
# do_request() возвращает Future-объект
push @req, $http->do_request( uri => $url );
}
return Future->wait_all( @req );
})
->on_done( sub {
$stream->write($_->get->content) for @_;
$stream->close_when_empty();
})
}
В данном примере выполнение операций выстраивается в последовательную цепочку:
- Вычитывается поток.
- Выполняется создание http-запросов по заданным url и возвращается Future-объект, который выполнится при условии завершения всех http-запросов.
- Финальный результат отправляется клиенту, и соединение завершается.
Если бы потребовалось добавить какие-то дополнительные операции после выполнения http-запроса, то они бы последовательно добавлялись в данную цепочку, ясно и чётко демонстрируя логику программы.
Датаграммы и прямая работа с сокетом
В отличии от IO::Async::Stream, IO::Async::Socket предназначен для работы с датаграммами, т.е. дискретными сообщениями.
use IO::Async::Socket;
use IO::Async::Loop;
my $loop = IO::Async::Loop->new;
# Подключение по протоколу UDP на порт 1234
$loop->connect(
host => "127.0.0.1",
service => "1234",
socktype => 'dgram',
# Успешное подключение
on_connected => sub {
my ( $sock ) = @_;
# Непосредственная работа с сокетом
my $socket = IO::Async::Socket->new(
handle => $sock,
# получен пакет
on_recv => sub {
my ( $self, $dgram, $addr ) = @_;
print "Получена датаграмма: $dgram\n",
$loop->stop;
},
# ошибка при отправке в сокет
on_recv_error => sub {
my ( $self, $errno ) = @_;
die "Ошибка - $errno\n";
},
);
$loop->add( $socket );
# Отправка сообщения в сокет
$socket->send( "Привет, мир!" );
},
on_resolve_error => sub { die "Cannot resolve - $_[0]\n"; },
on_connect_error => sub { die "Cannot connect\n"; },
);
$loop->run;
В указанном примере создаётся объект класса IO::Async::Socket, который становится наблюдателем за сокетом соединения. Функция-колбек on_recv вызывается каждый раз при получение порции данных (как правило с приходом каждого пакета). IO::Async::Socket можно использовать и для работы с сокетом типа SOCK_STREAM, в этом случае поток данных обрабатывается дискретно, т.е. по мере поступления порций данных в сокет.
Таймеры
Таймеры позволяют запустить некоторую операцию в заданный момент времени. В примере сетевого сервера таймер мог быть полезен для отслеживания висящих без дела клиентов.
sub on_stream {
my $stream = shift;
# Запускаем сторожевой таймер на 5 секунд
my $watchdog = IO::Async::Timer::Countdown->new(
delay => 5,
# Закрываем поток по истечении времени
on_expire => sub {
$stream->close;
}
);
$watchdog->start();
$loop->add( $watchdog );
$stream->configure(
on_read => sub {
my ( $self, $buffref, $eof ) = @_;
# Сброс таймера, если данные появились
$watchdog->reset;
...
},
on_closed => sub {
# Остановить таймер, если поток закрылся
$watchdog->stop;
}
);
$loop->add( $stream );
...
}
Помимо IO::Async::Timer::Countdown существуют таймеры IO::Async::Timer::Absolute (для запуска задачи в указанное время) и IO::Async::Timer::Periodic (для периодического запуска задачи).
Сигналы
С помощью IO::Async::Signal существует возможность асинхронно обрабатывать поступающие сигналы, выполняя заданный код обработчика сигнала. При обычном способе задания обработки сигнала, через установку $SIG{NAME}, может произойти ситуация, когда сигнал придёт в середине выполнения какой-либо важной транзакции, и код обработчика потенциально способен нарушить её. Реализация IO::Async::Signal гарантирует выполнение кода обработчика после завершения текущего такта цикла, т.о. происходит синхронизация относительно основного цикла событий.
Допускается установка множества обработчиков для одного сигнала. В этом случае каждый из них будет выполнен, но без какого-либо фиксированного порядка.
Пример обработки сигнала SIGHUP:
use IO::Async::Signal;
use IO::Async::Loop;
my $loop = IO::Async::Loop->new;
my $signal = IO::Async::Signal->new(
name => "HUP",
# Завершить цикл при получении сигнала SIGHUP
on_receipt => sub {
print "Stop loop after SIGHUP\n";
$loop->stop;
},
);
$loop->add( $signal );
$loop->run;
Возможен также альтернативный вариант создания обработчиков сигналов, через метод attach_signal в IO::Async::Loop:
use IO::Async::Loop;
my $loop = IO::Async::Loop->new;
my $sig_id = $loop->attach_signal( "SIGHUP", sub {
print "Stop loop after SIGHUP\n";
$loop->stop;
});
$loop->run;
Метод detach_signal позволяет удалить обработчик:
$loop->detach_signal($sig_id);
Управление процессами
Одно из важных условий корректной работы цикла обработки событий — неблокируемый код и отсутствие процессороёмких вычислений. В тех случаях, когда избежать этого невозможно, можно прибегнуть к выполнению таких операций в отдельном процессе или треде.
В IO::Async существуют несколько способов для запуска и управления дочерними процессами.
IO::Async::Process
IO::Async::Process позволяет запустить внешнюю программу или код. Модуль выполняет последовательные вызовы fork() и exec(), если это внешняя команда, или fork() и eval() в скалярном контексте, если вызывается код.
В случае выполнения внешней программы, существует множество способов для получения stdout, stderr запущенного процесса или передачи данных на stdin дочернего процесса.
Например, простой пример для захвата stdout дочернего процесса:
use IO::Async::Loop;
use IO::Async::Process;
my $loop = IO::Async::Loop->new;
my $stdout;
my $process = IO::Async::Process->new(
command => [ "writing-program", "arguments" ],
stdout => { into => \$stdout },
on_finish => sub {
print "The process has finished, and wrote:\n";
print $stdout;
}
);
$loop->add( $process );
$loop->run
Пример посложнее, для вычисления с помощью консольного калькулятора bc. Выражение передаётся на stdin процесса, а результаты забираются из stdout:
use IO::Async::Loop;
use IO::Async::Process;
my $loop = IO::Async::Loop->new;
my $process = IO::Async::Process->new(
command => [ "bc", "-q" ],
# Формируется pipe, дочерний процесс читает stdin и пишет на stdout
stdio => {
via => "pipe_rdwr"
},
on_finish => sub {
$loop->stop;
},
);
# Чтение данных в родительском процессе
$process->stdio->configure(
on_read => sub {
my ( $stream, $buffref ) = @_;
while( $$buffref =~ s/^(.*\n)// ) {
print "Answer is $1";
}
return 0;
},
);
$loop->add( $process );
# Отправка выражений для вычислений в bc
$process->stdio->write("$_\n") for ("2+2", "2+2*2", "(2+2)*2" );
$loop->run
На выходе программы получим такой результат:
Answer is 4
Answer is 6
Answer is 8
Выполнение кода позволяет получить только код завершения процесса:
my $process = IO::Async::Process->new(
code => sub {
return 42
},
on_finish => sub {
my ($self, $code) = @_;
printf "Exit code: %d\n", $code>>8;
},
);
...
Методы IO::Async::Loop
Как и во многих других случаях, создание дочерних процессов продублировано в реализации IO::Async::Loop. Например, метод open_child() является обёрткой вокруг IO::Async::Process:
$pid = $loop->open_child(
command => [...],
on_finish => sub { ... }
);
Кроме того, присутствует метод run_child, который является упрощённым способом вызова внешних процессов, если требуется получить только их вывод:
$loop->run_child(
command => [ "ls", "-1" ]
on_finish => sub {
my ( $pid, $exitcode, $stdout, $stderr ) = @_;
my $status = ( $exitcode >> 8 );
...
},
);
IO::Async::PID
IO::Async::PID позволяет отслеживать завершение работы дочернего процесса. Может быть полезен, если по каким-то причинам вас не устроили все предыдущие методы запуска внешних процессов.
use IO::Async::PID;
use IO::Async::Loop;
my $loop = IO::Async::Loop->new;
# Запуск дочернего процесса
my $kid = $loop->fork(
code => sub {
print "Сон..\n";
sleep 10;
print "Выход\n";
return 20;
},
);
print "Запущен дочерний процесс $kid\n";
my $pid = IO::Async::PID->new(
pid => $kid,
on_exit => sub {
my ( $self, $exitcode ) = @_;
printf "Дочерний процесс %d завершился с кодом %d\n",
$self->pid, $exitcode>>8;
},
);
$loop->add( $pid );
$loop->run;
Асинхронный запуск функций с помощью IO::Async::Function и IO::Async::Routine
В разделе, посвящённом IO::Async::Process упоминалось, что дочерний процесс может выполнять и фрагмент кода, но функционал этот достаточно бедный, т.к. позволяет вернуть только код завершения процесса из процесса-потомка.
Для асинхронного выполнения функций с возможностью получения результатов выполнения в IO::Async созданы специальные классы: IO::Async::Function и IO::Async::Routine.
IO::Async::Function
Объект IO::Async::Function представляет собой функцию, которая будет выполняться параллельно основному процессу программы. Существует возможность выбора модели запуска — это либо fork нового процесса, либо создание нового треда. При первом старте или вызове такой функции происходит запуск одного или нескольких (в зависимости от настроек) процессов/нитей — рабочих. Эти рабочие содержат код выполняемой функции и могут многократно вызывать её и возвращать результат обратно в основной процесс, поэтому важно, чтобы функция обладала свойством реентерабельности, т.е. не изменяла своих внутренних структур, что могло бы изменить её поведение при повторных вызовах. Помимо количества рабочих можно задавать и таймаут, после которого неиспользуемый рабочий будет остановлен для экономии ресурсов.
Внутренне IO::Async::Function построен на базе IO::Async::Routine, о котором будет рассказано ниже. Основной процесс запускает процесс/нить рабочего и формирует каналы для ввода/вывода. Через входной канал рабочего передаются аргументы функции, а через выходной — возвращаемые данные. Для сериализации/десериализации данных используются функции модуля Storable.
Пример функции:
use IO::Async::Function;
use IO::Async::Loop;
use Crypt::CBC;
my $loop = IO::Async::Loop->new;
my $cipher = Crypt::CBC->new(-key => 'secret', -cipher => 'Blowfish');
my $function = IO::Async::Function->new(
# Код шифровщика
code => sub {
$cipher->encrypt(shift);
},
model => 'fork', # форк
max_workers => 10, # максимальное число рабочих
min_workers => 2, # минимальное число рабочих
idle_timeout => 10, # выключение неиспользуемых рабочих
# по таймауту
);
$loop->add( $function );
# Вызов функции
$function->call(
# Аргументы
args => [ "Plain text" ],
on_return => sub {
my $ciphertext = shift;
print "'Plain text' encrypted to '". $ciphertext ."'\n";
},
on_error => sub {
warn "Все CPU перегреты\n";
},
);
$loop->run;
Основное назначение таких функций: выполнение каких-либо интенсивных вычислений или блокирующихся операций, которым не требуется хранить информацию о состоянии между вызовами. Т.е. в идеале функции, результат которых зависит только от входных аргументов.
IO::Async::Routine
IO::Async::Routine также как и IO::Async::Function позволяет выполнять код во внешнем процессе/нити. Отличие состоит в том, что создаваемый рабочий запускается один раз и производит обмен данными с основным процессом через специальные каналы IO::Async::Channel. Такой рабочий может иметь сложную логику, хранить информацию о состоянии в процессе обмена с основным процессом и хорошо подходит для организации потоковой обработки данных.
Рассмотрим пример программы:
use IO::Async::Routine;
use IO::Async::Channel;
use IO::Async::Loop;
my $loop = IO::Async::Loop->new;
# Каналы для обмена данными
my $input = IO::Async::Channel->new;
my $output = IO::Async::Channel->new;
# Функция рабочего процесса
my $routine = IO::Async::Routine->new(
channels_in => [ $input ],
channels_out => [ $output ],
code => sub {
my @nums = @{ $input->recv };
my $ret = 0; $ret += $_ for @nums;
$output->send( \$ret );
},
on_finish => sub {
print "Рабочий завершил работу - $_[-1]\n";
$loop->stop;
},
);
$loop->add( $routine );
# Отправка данных рабочему
$input->send( [ 10, 20, 30 ] );
# Функция-колбек для приёма данных
$output->recv(
on_recv => sub {
my ( $ch, $totalref ) = @_;
print "10 + 20 + 30 = $$totalref\n";
$loop->stop;
}
);
$loop->run;
В примере создаются два канала IO::Async::Channel для обмена данными с рабочим. Данные объекты ориентированы на использование исключительно совместно с IO::Async::Routine, поэтому их не требуется вручную добавлять в цикл обработки событий, их управлением займётся сам IO::Async::Routine. Для этого требуется указать их в списках каналов channels_in и channels_out, т.е. определить направление записи в канал: входной и выходной каналы для рабочего соответственно.
Канал позволяет передавать только ссылки. Все такие ссылки в момент передачи сериализируются с помощью Storable, поэтому после того как объект сериализован, любые изменения в нём уже не будут видны тому процессу, в который он был передан.
Канал имеет два метода: send() и recv() для передачи и получения данных в/из канала соответственно. Выполнение send() происходит без блокировки. recv() блокируется в том случае, если вызван без указания колбека on_recv и возвращает полученные данные.
Изменения в файловой системе
Модуль IO::Async::File позволяет создавать уведомители об изменениях в файлах и каталогах. Наблюдать можно за изменениями любой характеристики файла, доступной в вызове stat(): время создания/доступа/модификации, размер, права доступа, владелец и другие.
use IO::Async::File;
use IO::Async::Loop;
my $loop = IO::Async::Loop->new;
# Наблюдаем за изменением времени модификации
# файла исходного кода программы
my $file = IO::Async::File->new(
filename => __FILE__,
on_mtime_changed => sub {
my ( $self, $new_mtime, $old_mtime ) = @_;
print "Время модификации файла изменилось",
"с mtime $old_mtime на $new_mtime\n";
}
);
$loop->add( $file );
$loop->run;
Так же есть возможность наблюдать за всеми характеристиками одновременно, указав функцию-колбек on_stat_changed, которая вызывается при изменении любой из характеристик. В качестве параметров передаются объекты класса File::stat с текущим и предыдущим состояниями файлового дескриптора.
Внутри модуль реализован на простом периодическом вызове функции stat() на файле каждые 2 секунды. Регулировать интервал опроса можно с помощью параметра interval при создании объекта.
ООП подход при использовании IO::Async
Пример сетевого сервера, рассмотренный в начале статьи, можно переделать с применением ООП подхода. Метод listen IO::Async::Loop возвращает объект IO::Async::Listener. Мы можем создать свой собственный класс MyListener, который будет наследовать методы IO::Async::Listener и будет иметь свой обработчик события on_stream:
use IO::Async::Loop;
my $loop = IO::Async::Loop->new;
# Создание объекта уведомителя MyListener,
# производного от IO::Async::Listener
my $myl = MyListener->new;
$loop->add($myl);
$myl->listen(
family => "inet",
socktype => "stream",
service => 1234,
on_resolve_error => sub { print STDERR "Cannot resolve - $_[0]\n"; },
on_listen_error => sub { print STDERR "Cannot listen\n"; },
);
$loop->run;
# Собственный модуль обработки потока
package MyListener;
# Наследуем от класса IO::Async::Listener
use base qw( IO::Async::Listener );
# Реализуем метод on_stream, вызываемый при подключении клиента
sub on_stream {
my ($self, $stream) = @_;
$stream->configure(
on_read => sub { 0 },
close_on_read_eof => 0
);
# Добавляем поток в цикл
$self->loop->add( $stream );
# Возвращается Future-объект для операции чтения потока до EOF
my $f = $stream->read_until_eof;
# Функция-колбек при завершении операции
$f->on_done(sub {
my ($buf, $eof) = @_;
while( $buf =~ s/^(.*)\n// ) {
$stream->write(scalar reverse "\n".$1);
}
$stream->close_when_empty() if $eof;
return 0;
});
}
1;
Такой подход позволяет получить логичный набор классов для каждой решаемой задачи и избежать появления огромных простыней кода с вложенными функциями-колбеками.
Заключение
Хочется отметить, что несмотря на то, что это ещё один фреймворк для СОП, он, тем не менее, заслуживает серьёзного внимания уже благодаря полноте реализуемых задач, а также уникальной интеграции событийно-ориентированного программирования с обещаниями и объектно-ориентированным подходом к компоновке программы.
Как уже было отмечено, цикл IO::Async может использовать множество реализаций мультиплексоров под капотом, что делает его похожим на AnyEvent. В экосистеме модулей IO::Async есть также модули для асинхронного разрешения имён с использованием системного резолвера, модуль http-клиента, прозрачная поддержка SSL и т.д. IO::Async можно использовать в веб-приложениях Mojolicious. Всё это позволяет строить широкий класс приложений на его основе.
← Про опыт разработки на Perl под Raspberry PI | Содержание | Обход дерева директорий на Perl и Haskell (часть 1) →
