Выпуск 4. Июнь 2013
← Введение в разработку web-приложений на PSGI/Plack. Часть 3. Starman. | Содержание | Что нового в Perl 5.18.0 →AnyEvent и fork
Довольно часто возникает задача выполнения некоторых действий в отдельном процессе, например, для исполнения блокирующихся операций или запуска внешних программ. fork отлично выполняет свою задачу, но если ваше приложение построено на базе AnyEvent, необходимо знать о некоторых нюансах, прежде чем начинать создавать новые процессы.
Fork
Классическим способом создания новых процессов в UNIX-подобных системах является выполнение системного вызова fork
, который создаёт копию выполняемого процесса и начинает выполнять оба процесса, начиная с последующей инструкции. Интерпретатор Perl просто выполняет системный вызов, если он доступен, а в случае его отсутствия — выполняет эмуляцию, которая в общем случае абсолютно прозрачна для программиста (с небольшими оговорками, смотрите perlfork). В современных операционных системах fork
оптимизирован и не выполняет копирования всего адресного пространства процесса, выполняя копирование лишь при изменении данных в процессе-потомке (механизм COW — копирование-при-записи). Так или иначе, оба процесса имеют в своём распоряжении все данные, располагавшиеся в родительском процессе, что в том числе касается и открытых файловых дескрипторов и директорий. Если выполняемый родительский процесс являлся многопоточной программой и в ней работали несколько нитей, то копируется только нить, которая запустила системный вызов, остальные нити в дочернем процессе бесследно исчезают.
Для запуска нового приложения применяется системный вызов из семейства exec
, который замещает образ текущего процесса новым. По этой причине для параллельного запуска новой программы совместно с работающей используют последовательность вызовов fork
и затем exec
в процессе-потомке. При выполнении exec
все используемые данные старого процесса освобождаются, но новый процесс наследует те же самые pid и ppid старого процесса, и, кроме того, в новый процесс копируются все открытые файловые дескрипторы (если они не были открыты с флагом O_CLOEXEC
).
Оба варианта запуска новых процессов могут быть использованы в Perl для выполнения параллельных задач.
Проблемы использования fork
Описанное поведение fork
может оказывать нежелательные побочные эффекты на работу приложения. Рассмотрим проблемы, которые могут возникнуть в общем случае, и проблемы, которые специфичны для AnyEvent
-приложений.
- Медленное создание копии крупного процесса
Если объём памяти, занимаемой приложением, исчисляется несколькими гигабайтами, то создание копии процесса может занять ощутимое время. Несмотря на наличие механизма COW, создание процесса по-прежнему требует инициации структуры нового процесса в ядре, копирования таблиц страниц памяти родительского процесса, которые могут занимать существенный объём для огромного процесса. Это также может усугубляться тем, что единственной требуемой операцией в дочернем процессе будет создание нового процесса через вызов exec
. Это приводит к напрасной трате ресурсов и большим накладным расходам для создания новых процессов.
- Копирование бесполезных для дочернего процесса данных
Данные, копируемые в дочерний процесс, могут быть абсолютно бесполезны для дочернего процесса. Благодаря механизму COW они не занимают дополнительного объёма памяти, но это так, пока родительский процесс тоже их не изменяет. В тот самый момент, когда родительский процесс освободит память под уже ненужные данные или обновит их, старая версия этих данных перетечёт в дочерний процесс. Получим парадоксальную ситуацию, когда освобождённая память не освобождается, а просто утекает в другой процесс.
- Fork может сделать невозможным работу дочернего процесса
Программа может использовать нити через модуль threads
или загружать модули, которые используют POSIX threads неявно, например модуль IO::AIO
. Специфика клонирования многопоточного процесса может привести к тому, что полученный дочерний процесс окажется в неконсистентном состоянии, препятствующем его дальнейшей корректной работе. Такая ситуация возможна, даже если вы не используете нити, а вызываете fork
внутри обработчика сигнала, который может выполниться в произвольной точке программы, разорвав, например, критичную транзакцию.
- Обработка событий в дочернем процессе
При создании дочернего процесса в него копируются все действующие объекты, наблюдающие за событиями. Не все событийные библиотеки, работающие в бэкенде AnyEvent
, способны корректно начать работать в дочернем процессе. Поэтому обработка событий в дочернем процессе может оказаться невозможной. Даже если библиотека обработки событий гарантирует работоспособность после fork
, в дочернем процессе могут начать запускаться таймеры или обработчики сигналов, которые имели смысл только в родительском процессе.
Fork
+exec
нового процесса интерпретатора Perl может быть не так прост
Не всегда легко обнаружить правильный путь к интерпретатору Perl, в переменной ^X
может содержаться совсем не интерпретатор.
Fork
+exec
нового процесса может оказаться неудачным
Часто процесс может выполняться очень длительное время, за которое в окружении могут произойти перемены, например, обновиться модули или даже сам интерпретатор. Попытка fork
+exec
нового процесса может завершиться ошибкой, например, из-за проблемы в обновлённом стороннем модуле.
AnyEvent::Fork
Недавно вышедший модуль AnyEvent::Fork
Марка Леменна (Marc Lehmann) пытается решить все указанные выше проблемы использования fork
в AnyEvent
-приложениях, предоставляя безопасный и гибкий инструмент создания и управления процессами.
Особенность модуля в том, что он создаёт новые процессы путём запуска нового интерпретатора perl или посредством клонирования существующего “шаблонного” процесса. Такой подход позволяет решить половину указанных проблем, связанных с разделением общих данных в процессах (2, 3 и 4).
Для решения проблемы со скоростью работы fork
+exec
в нём используется модуль Proc::FastSpawn
, который при наличии поддержки на платформе применяет системные вызовы vfork
+exec
. Вызов vfork
аналогичен fork
с той лишь разницей, что не происходит копирования структур данных родительского процесса в процесс-потомок. При этом родительский процесс останавливается до тех пор, пока дочерний процесс не завершится или не выполнит вызов exec
. Это значительно ускоряет запуск нового процесса. На платформе win32
и других, где нет вызова vfork
, по возможности используется spawn
или происходит откат на обычный fork
+exec
.
Модуль также пытается обнаружить нужный интерпретатор perl, анализируя глобальную переменную ^X
, и в случае проблем откатывается на использование $Config::Config{perlpath}
. Кроме того, существует возможность переопределить путь к интерпретатору через глобальную переменную $AnyEvent::Fork::PERL
.
Чтобы избежать проблем с обновлённым окружением для долгоработающих процессов, AnyEvent::Fork
предлагает использование шаблонного процесса, который запускается в самом начале работы программы и позже при необходимости используется для создания новых процессов через fork
. Кроме того, это решает и проблему, появившуюся с использованием exec
, когда требуется каждый раз загружать модули, используемые программой, — использование шаблонного процесса позволяет загрузить необходимые модули один раз.
Использование AnyEvent::Fork
Рассмотрим типичный пример использования:
1use :: ;23::4->new5->require ("MyModule")6-> ("MyModule::server", my $cv = :: );78my $fh = $cv->recv;
Объект AnyEvent::Fork
создаётся вызовом метода new()
. Метод new()
помимо создания объекта производит запуск нового процесса интерпретатора perl, который становится “шаблонным” процессом. Из этого шаблона в дальнейшем и формируются все новые процессы посредством вызова fork
.
Вызов метода require()
позволяет выполнить загрузку модуля в процессе потомка. Метод run()
выполняет подпрограмму в процессе-потомке. Первым параметром указывается имя подпрограммы, вторым параметром передаётся колбэк-функция, которая выполняется после того, как будет запущен процесс потомок.
Первым параметром, который получают запускаемая в новом процессе подпрограмма и колбэк-функция в родительском процессе, — это дескриптор сокета, открытого между процессами. Если использование сокета не требуется, его можно закрыть в этих подпрограммах.
Передача параметров и файловых дескрипторов в дочерний процесс
Для того, чтобы передать дополнительные параметры в подпрограмму, вызываемую в дочернем процессе, используется метод send_arg()
:
use ::;1use :: ;23::4->new5->eval('6sub runner {
7my ($fh, @params) = @_;
8...
9}
10')
11-> (@params)12-> ("runner", my $cv = :: );1314my $fh = $cv->recv;
Метод передаёт только строковые параметры, а для передачи сложных структур данных требуется использовать сериализацию.
Также существует возможность передавать файловые дескрипторы с помощью метода send_fh()
:
open my $log, '>', '/var/log/file' or die $!;151open my $log, '>', '/var/log/file' or die $!;23::4->new5->eval('6sub runner {
7my ($fh, $log, @params) = @_;
8...
9}
10')
11-> ($log)12-> (@params)13-> ("runner", my $cv = :: );1415my $fh = $cv->recv;
Это открывает широкие возможности для коммуникации между процессами родителя и потомка.
Создание и управление пулом процессов
Метод fork()
используется для клонирования нужного шаблонного процесса:
my $pool =1my $pool =2::3->new4->require ("MyModule");56my @pool;78for my $id (1..10) {9push @pool,10$pool
11->fork12-> ($id)13-> ("MyModule::foo", sub {1415my ($fh) = @_;16print "Process id $id\n";17...
18});19}
Программа создаст пул из десяти процессов из заданного шаблонного процесса.
Работа модуля при отсутствии возможности запуска нового интерпретатора
В некоторых ситуациях бывает невозможно запустить новый интерпретатор perl. Например, он может быть встроенным в другое приложение, которое сейчас выполняет этот код, или выполнение fork
может испортить работу какого-либо уже загруженного модуля.
Для этих случаев в состав дистрибутива AnyEvent::Fork
включены два модуля: AnyEvent::Fork::Early
и AnyEvent::Fork::Template
.
AnyEvent::Fork::Early
должен быть загружен в самом начале вашего приложения до загрузки каких-либо других библиотек. В этом случае происходит форк текущего процесса, который затем и используется для создания новых процессов:
#!/usr/bin/perl1#!/usr/bin/perl
2use :::: ;34# some other code
Действие, которое выполняет AnyEvent::Fork::Template
, — практически то же самое, только перед созданием шаблонного процесса есть возможность загрузить некоторые дополнительные модули, которые станут доступны во всех вновь создаваемых процессах:
use ::::;1use :::: ;2use :::: ;34use :::: ;56use - ;78$AnyEvent::::->fork-> ("My::Worker::Module::run_worker", sub { ... });
В данном примере после создания шаблона загружается модуль Gtk2
, в котором могут возникнуть проблемы при использовании fork
. В последней строке происходит создание нового процесса из шаблонного процесса, в котором не загружен “опасный” модуль Gtk2
.
AnyEvent::Fork::RPC
В состав модуля AnyEvent::Fork
не встроено никаких средств для создания очереди заданий или протокола двустороннего обмена между процессом-потомком и родительским процессом в виде запросов/ответов. Поэтому был создан модуль AnyEvent::Fork::RPC, который предназначен именно для подобных целей. На текущий момент его API имеет статус экспериментального, поэтому вполне вероятны изменения.
Пример использования:
use ;1use ;2use :::: ;34my $done = :: ;56my $rpc = ::7->new8->eval(do { local $/; <> })9->:::::: ("worker",10=> sub { warn "FATAL: $_[0]"; exit 1 },11=> sub { warn "$_[0] requests handled\n" },12=> $done,13);1415for my $id (1..100) {16$rpc->( "foo" => "bar", sub {17$_[0] or warn "$id: $_[1]\n";18});19}2021undef $rpc;2223$done->recv;2425__DATA__
2627my $count;
2829sub worker {
30my ($command, $data) = @_;
3132AnyEvent::Fork::RPC::event ($count) unless ++$count % 10;
3334my $status = ...
3536$status or (0, "$!")
37}
После обычного создания объекта нового процесса выполняется метод AnyEvent::Fork::RPC::run
, возвращающий объект, который можно будет использовать для многократного выполнения вызовов подпрограммы в дочернем процессе. Существует возможность регистрации функций-колбэков на различные события, которые будут происходить при запуске подпрограммы: ошибки, уничтожение объекта и т.п.
В данном примере происходит последовательное заполнение очереди команд на запуск подпрограммы и регистрации функций-колбеков для получения результатов выполнения команды в родительском процессе. После формирования очереди объект $rpc
освобождается присвоением переменной значения undef
. Это приведёт к тому, что после выполнения всей очереди заданий процесс потомок будет завершён. Это в свою очередь приведёт к вызову колбэка уничтожения объекта (on_destroy
).
AnyEvent::Fork::Pool
Если в вашем приложении требуется более тонкое управление пулом процессов, то для этих целей можно использовать модуль AnyEvent::Fork::Pool. Так же как и предыдущий модуль, API данного модуля сейчас находится в альфа-стадии и может активно изменяться.
Пример использования:
use ;1use ;2use :::: ;34my $pool = ::5->new6->require ("MyWorker")7->:::::: (8"MyWorker::run", # the worker function910# pool management
11=> 4, # absolute maximum # of processes12=> 0, # minimum # of idle processes13=> 2, # queue at most this number of jobs per process14=> 0.1, # wait this many seconds before starting a new process15=> 10, # wait this many seconds before stopping an idle process16=> (my $finish = :: ), # called when object is destroyed1718# parameters passed to AnyEvent::Fork::RPC
19=> 0,20=> sub { die "FATAL: $_[0]\n" },21=> sub { my @ev = @_ },22=> "MyWorker::init",23=> $AnyEvent:::::: ,24);2526for (1..10) {27$pool->( => $_, sub {28print "MyWorker::run returned @_\n";29});30}3132undef $pool;3334$finish->recv;
В данном примере создаётся пул процессов, которые выполняют функцию MyWorker::run
. В соответствии с заданными настройками AnyEvent::Fork::Pool
автоматически регулирует количество необходимых для выполнения заданий процессов, время их работы и необходимость уничтожения.
Параметры для управления пулом:
- idle — указывает минимальное число простаивающих процессов; если количество свободных процессов снижается меньше этого уровня, происходит запуск новых процессов;
- max — максимальное число процессов в пуле;
- load — максимальное количество задач, отправляемых на один рабочий процесс;
- start — задержка запуска нового процесса, в случае, если в соответствии с настройками требуется запуск нового процесса;
- stop — время, через которое рабочий процесс будет остановлен, если он простаивает.
Заключение
Модуль AnyEvent::Fork
и вспомогательные модули на его основе позволяют безопасно и гибко управлять созданием новых процессов для решения задач по параллельной обработке данных. Успешно решены проблемы с побочными эффектами fork
для асинхронных приложений и реализовано универсальное и кроссплатформенное решение.
Возможно, ещё преждевременно рекомендовать модули для использования в промышленной эксплуатации, поскольку API модулей ещё не стабилизированы, но определённо есть смысл начать изучать и экспериментировать. Удачных опытов!
← Введение в разработку web-приложений на PSGI/Plack. Часть 3. Starman. | Содержание | Что нового в Perl 5.18.0 →