Почему обратные вызовы AnyEvent::child никогда не запускаются, если события интервального таймера всегда готовы?

Обновите. Эту проблему можно решить с помощью исправлений, представленных в https://github.com/zbentley/AnyEvent-Impl-Perl-Improved/tree/io-starvation

Контекст:

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

Я использую основанный на чистом perl AnyEvent::Loop цикл обработки событий, который на данный момент достаточно хорош для моих целей; большая часть того, что мне нужно, это отслеживание сигналов/процессов/таймеров.

Проблема:

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

use AnyEvent;

# Start a timer that, every 0.5 seconds, sleeps for 1 second, then prints "timer":
my $w2 = AnyEvent->timer(
    after => 0,
    interval => 0.5,
    cb => sub {
        sleep 1; # Simulated blocking operation. If this is removed, everything works.
        say "timer";
    },
);

# Fork off a pid that waits for 1 second and then exits:
my $pid = fork();
if ( $pid == 0 ) {
    sleep 1;
    exit;
}

# Print "child" when the child process exits:
my $w1 = AnyEvent->child(
    pid => $pid,
    cb => sub {
        say "child";
    },
);

AnyEvent->condvar->recv;

Этот код оставляет дочерний процесс зомбированным и печатает «таймер» снова и снова, «навсегда» (я запускал его несколько минут). Если вызов sleep 1 удален из обратного вызова для таймера, код работает правильно, и наблюдатель дочернего процесса срабатывает, как и ожидалось.

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

sleep 1 может быть любой блокирующей операцией. Его можно заменить занятым ожиданием или любой другой вещью, которая занимает достаточно много времени. Это даже не должно занять секунду; кажется, что это должно быть только а) запущено во время события дочернего выхода/доставки SIGCHLD и б) привести к тому, что интервал всегда должен запускаться в соответствии с настенными часами.

Вопросы:

Почему AnyEvent никогда не запускает обратный вызов моего дочернего процесса?

Как я могу мультиплексировать события выхода дочернего процесса с интервальными событиями, которые могут блокироваться так долго, что наступит следующий интервал?

Что я пробовал:

Моя теория заключается в том, что события таймера, которые становятся «готовыми» из-за времени, проведенного вне цикла событий, могут на неопределенный срок опережать другие типы готовых событий (например, наблюдатели за дочерними процессами) где-то внутри AnyEvent. Я пробовал несколько вещей:

  • Использование AnyEvent::Strict не выявляет ошибок и не изменяет поведение каким-либо образом.
  • Частичное решение: удаление интервального события в любой момент действительно приводит к срабатыванию наблюдателя дочернего процесса (как если бы внутри AnyEvent выполнялся какой-то внутренний опрос событий/заполнение очереди, что происходит только в том случае, если нет уже готовых событий таймера). "по настенным часам). Недостатки: в общем случае это не работает, так как мне нужно знать, когда мой дочерний процесс завершился, чтобы знать, когда отложить мои интервалы, что является тавтологией.
  • Частичное решение: в отличие от наблюдателей за дочерними процессами, другие интервальные таймеры, по-видимому, могут прекрасно мультиплексироваться друг с другом, поэтому я могу установить ручной вызов waitpid в другом интервальном таймере, чтобы проверять и собирать дочерние процессы. Недостатки: ожидание дочерних элементов может быть искусственно отложено (мой вариант использования включает в себя множество частых процессов создания/уничтожения), любые AnyEvent::child наблюдатели, которые установлены и успешно срабатывают, автоматически пожинают дочерние элементы и не сообщают мой интервал /waitpid timer, требующий оркестровки, и обычно мне кажется, что я неправильно использую AnyEvent.

person Zac B    schedule 12.06.2016    source источник
comment
Я не знаю, имеет ли это значение, но в документации AnyEvent сказано следующее Это означает, что вы не можете создать дочерний наблюдатель в качестве самого первого элемента в программе AnyEvent, вы должны создать по крайней мере один наблюдатель, прежде чем разветвлять дочерний элемент.   -  person Borodin    schedule 12.06.2016
comment
Нет, поведение остается прежним, даже если наблюдатель за таймером создается до форка. Я обновлю свой пример, чтобы сделать это; Спасибо за информацию!   -  person Zac B    schedule 12.06.2016


Ответы (2)


Интервал — это время между началом каждого обратного вызова таймера, т. е. не время между окончанием обратного вызова и началом следующего обратного вызова. Вы устанавливаете таймер с интервалом 0,5, и действие таймера — переход в спящий режим на одну секунду. Это означает, что после срабатывания таймера он будет немедленно запускаться снова и снова, потому что интервал всегда заканчивается после возврата таймера.

Таким образом, в зависимости от реализации цикла событий может случиться так, что никакие другие события не будут обрабатываться, потому что он занят запуском одного и того же таймера снова и снова. Я не знаю, какой базовый цикл событий вы используете (отметьте $AnyEvent::MODEL), но если вы посмотрите на исходный код AnyEvent::Loop (цикл для реализации на чистом Perl, т.е. модель AnyEvent::Impl::Perl) вы найдете следующий код:

   if (@timer && $timer[0][0] <= $MNOW) {
      do {
         my $timer = shift @timer;
         $timer->[1] && $timer->[1]($timer);
      } while @timer && $timer[0][0] <= $MNOW;

Как видите, он будет занят выполнением таймеров до тех пор, пока есть таймеры, которые необходимо запустить. И с вашей настройкой интервала (0,5) и поведением таймера (сон на одну секунду) всегда будет таймер, который нужно выполнить.

Если вместо этого вы измените свой таймер так, чтобы было место для обработки других событий, установив интервал больше, чем время блокировки (например, 2 секунды вместо 0,5), все работает нормально:

...
interval => 2,
cb => sub {
    sleep 1; # Simulated blocking operation. Sleep less than the interval!!
    say "timer";


...
timer
child
timer
timer
person Steffen Ullrich    schedule 12.06.2016
comment
Этот ответ кажется правильным, но также показывает, что этот цикл событий действительно сломан. Что, если операция блокировки в обратном вызове таймера может занять переменное время? Тогда есть шанс, что другие эмиттеры событий никогда не сработают, потому что их что-то опережает; нет основной очереди. Вздох. Пометка как принятая, с грустью :p - person Zac B; 13.06.2016
comment
@ZacB: событие дочернего выхода не теряется, оно просто не запускается, потому что вы заняты другими делами. Если вы прекратите делать другие вещи, событие будет доставлено. Это не проблема цикла событий, а то, что вы просите невозможного, то есть выполнения каждые 500 мс задачи, которая занимает одну секунду в однопоточном приложении. Это как пытаться купить товар за 100 долларов, когда у вас всего 50 долларов. - person Steffen Ullrich; 13.06.2016
comment
Кажется более разумным, чтобы интервальные таймеры работали по модели не чаще одного раза каждые X секунд и ставили события в очередь внутри. То есть функция цикла событий run() должна проверять таймеры и помещать обратные вызовы в очередь (сигналы и т. д., когда они срабатывают, также должны помещать обратные вызовы в очередь). После выполнения этого начального шага обнаружения события готовности обратные вызовы должны извлекаться и запускаться из очереди в порядке FIFO. - person Zac B; 13.06.2016
comment
@ZacB: Изменение цикла приведет к тому, что он будет вести себя по-другому, но по-прежнему нет возможности купить все товары за 100 долларов, если у вас есть только 50 долларов. Все, что вы изменили, это выбор товаров, которые вы можете купить. И то, что может быть более разумным выбором для вас, все же является изменением поведения, и это может негативно повлиять на других. Опять же: проблема не в цикле событий, а в том, что вы просите невозможного. - person Steffen Ullrich; 13.06.2016
comment
Я не думаю, что это так просто, как вы говорите; модель с очередями — это просто другой набор гарантий доставки событий. Интервал не обязательно должен запускать это примерно каждые $duration даже за счет других ожидающих событий_; это также может означать запуск этого каждые $duration, если нет ожидающих событий. Это можно имитировать, откладывая интервалы AnyEvent до следующего запуска события; Я поставлю это в отдельный ответ (ваш все еще правильный). - person Zac B; 15.06.2016
comment
@ZacB: в вашем коде прямо сказано, что он должен выполнять каждые 0,5 секунды задачи, которые выполняются в течение 1 секунды, вот как определяется интервал. Кроме того, вы ждете дочернего выхода. Что бы ни делал цикл событий, невозможно достичь всех заданных целей в одном многопоточном приложении, и поэтому реализация должна быть несправедливой по отношению к некоторым задачам. Вы жалуетесь, что реализация несправедливо справляется с этой невыполнимой задачей, потому что вы предпочитаете, чтобы она была несправедлива по-другому. На мой взгляд, это плохая идея — просить о невозможном вообще. - person Steffen Ullrich; 15.06.2016
comment
Вполне возможно создать циклы событий, которые имеют дело с этим сценарием. На самом деле, я бы даже сказал, что большинство циклов событий справляются с этим правильно. Ни один интервал не срабатывает точно по времени; все вычисления имеют накладные расходы. Если эти накладные расходы непредсказуемы, цикл обработки событий не должен истощать случайные генераторы. - person Zac B; 15.06.2016
comment
Большинство движков JavaScript (некоторые из существующих циклов событий, наиболее важных для надежности) используют очереди за стеками вызовов (обратных вызовов), которые они представляют пользователям, чтобы справиться с этой конкретной ситуацией (пользовательские обратные вызовы, которые могут блокировать цикл на произвольное время) . См.: developer.mozilla.org/en-US/docs/Web/ JavaScript/EventLoop stackoverflow.com/questions/21607692/< /а> - person Zac B; 15.06.2016
comment
@ZacB: опять же, дочернее событие выхода не потеряно. Как только вы остановите таймеры, событие будет доставлено. И очередь событий будет просто несправедливой по-разному, а в худшем случае просто будет становиться все длиннее и длиннее или просто пропускать неправильные события. Это просто не справедливость, если вы перерасходуете ресурсы, просто разные виды несправедливости. Если вам не нужен интервальный таймер, а вместо этого переустанавливаете таймер после завершения последнего таймера, вам следует именно это сделать, а не использовать интервальный таймер. - person Steffen Ullrich; 15.06.2016

Обновите. Эту проблему можно решить с помощью исправлений, представленных в https://github.com/zbentley/AnyEvent-Impl-Perl-Improved/tree/io-starvation

Ответ @steffen-ulrich правильный, но указывает на очень ошибочное поведение в AnyEvent: поскольку нет базовой очереди событий, определенные виды событий, которые всегда сообщают «готово», могут на неопределенный срок опережать другие.

Вот обходной путь:

Для интервальных таймеров, которые всегда «готовы» из-за блокирующей операции, происходящей за пределами цикла событий, можно предотвратить голодание, объединяя интервальные вызовы в цепочку со следующим запуском цикла событий, например так:

use AnyEvent;

sub deferred_interval {
    my %args = @_;
    # Some silly wrangling to emulate AnyEvent's normal
    # "watchers are uninstalled when they are destroyed" behavior:
    ${$args{reference}} = 1;
    $args{oldref} //= delete($args{reference});
    return unless ${$args{oldref}};

    AnyEvent::postpone {
        ${$args{oldref}} = AnyEvent->timer(
            after => delete($args{after}) // $args{interval},
            cb => sub {
                $args{cb}->(@_);
                deferred_interval(%args);
            }
        );
    };

    return ${$args{oldref}};
}

# Start a timer that, at most once every 0.5 seconds, sleeps
# for 1 second, and then prints "timer":
my $w1; $w1 = deferred_interval(
    after => 0.1,
    reference => \$w2,  
    interval => 0.5,
    cb => sub {
        sleep 1; # Simulated blocking operation.
        say "timer";
    },
);

# Fork off a pid that waits for 1 second and then exits:
my $pid = fork();
if ( $pid == 0 ) {
    sleep 1;
    exit;
}

# Print "child" when the child process exits:
my $w1 = AnyEvent->child(
    pid => $pid,
    cb => sub {
        say "child";
    },
);

AnyEvent->condvar->recv;

Используя этот код, наблюдатель за дочерними процессами сработает более или менее вовремя, и интервал будет продолжать срабатывать. Компромисс заключается в том, что каждый интервальный таймер запускается только после завершения каждого блокирующего обратного вызова. Учитывая время интервала I и время выполнения блокирующего обратного вызова B, этот подход будет запускать событие интервала примерно каждые I + B секунды, а предыдущий подход из вопроса займет min(I,B) секунд (за счет потенциального голодания).

Я думаю, что многих головных болей здесь можно было бы избежать, если бы у AnyEvent была резервная очередь (многие распространенные циклы обработки событий используют этот подход для предотвращения ситуаций, в точности подобных этой), или если бы реализация AnyEvent::postpone установила эмиттер событий, подобный NextTick. запускаться только после того, как все остальные эмиттеры будут проверены на наличие событий.

person Zac B    schedule 15.06.2016
comment
Я думаю, что это должен быть вызов deferred_interval, если неопределенный chain_interval. Но в любом случае это уже не интервальный таймер (т.е. фиксированная разница между таймерами), а таймер, который устанавливается после завершения исходного обратного вызова. Таким образом, вы не сможете обойти очень ошибочное поведение, но просто больше не будете ожидать невозможных вещей от цикла обработки событий. - person Steffen Ullrich; 15.06.2016
comment
Исправлено, спасибо! Опять же (хотя мы, вероятно, забили это до смерти в комментариях к вопросу), я думаю, что предсказуемая несправедливость (действительно просто справедливое планирование обратного вызова для полностью используемых циклов событий) значительно предпочтительнее по сравнению с голоданием других источников событий, основанных на источнике тип. Я думаю, что при запуске произвольного пользовательского кода надежная реализация цикла обработки событий должна делать предсказуемые вещи даже перед лицом неожиданной блокировки; очередь - один из способов обойти это. Явный приоритет эмиттера (на самом деле просто очередь приоритетов) - это другое. Потенциально-бесконечным голоданием не является. - person Zac B; 16.06.2016
comment
Для примера того, что я имею в виду, попробуйте запустить что-то вроде этого через Node (который имеет цикл событий очереди): pastebin.com/ AR09NqXw Цикл по-прежнему используется полностью (время блокировки для обратных вызовов больше, чем ширина любых установленных таймеров), но интервальные очереди не заполняются и не вызывают утечку памяти, а сигналы/немедленные события/и т. д. по-прежнему исключаются из очереди. и запустить в порядке FIFO. - person Zac B; 16.06.2016
comment
Раз уж вы хотите продолжить эту дискуссию... Опять же, вы ожидаете какой-то особой несправедливости, вероятно, потому, что вы привыкли именно к такой несправедливости. Но, на мой взгляд, основная проблема все же в том, что вы просите невозможного, т.е. программа не должна требовать, чтобы цикл событий был постоянно несправедливым. Текущее решение исправляет это, больше не требуя невозможного, что является правильным подходом. Опять же, дочернее событие выхода не теряется. Он не доставляется только до тех пор, пока вы держите цикл заняты другими задачами. - person Steffen Ullrich; 16.06.2016
comment
Я понимаю, что это не потеряно; вот почему я имею в виду голод, а не потери. Все циклы событий, по вашему определению, постоянно несправедливы: если два таймера (в одном и том же тике) запланированы на 1 секунду в будущем, один из двух обратных вызовов должен будет дождаться завершения другого, поэтому он выиграет. t огонь вовремя; Это нечестно. Предсказуемая несправедливость гораздо полезнее непредсказуемой; AnyEvent правильно обрабатывает случай с двумя таймерами (срабатывает один, а затем другой), но некоторые другие типы эмиттеров (сигналы) не обрабатываются таким же образом. Это непоследовательно и опасно. - person Zac B; 16.06.2016
comment
Одним из преимуществ циклов событий по сравнению с точным планированием (потоками) является то, что они могут мультиплексировать множество обратных вызовов, даже если они полностью используются/никогда не блокируют ожидание событий. Мультиплексору будет присуща некоторая степень несправедливости, и он должен свести ее к минимуму (например, используя внутренние часы, чтобы отложенные таймеры не ждали user time + timer time, поэтому мой обходной путь является плохим решением), но в некоторой степени предсказуемость или справедливость являются ключевыми. Многие циклы событий предназначены для работы с полной загрузкой: события готовы каждый раз, когда пользовательские обратные вызовы возвращают управление циклу. - person Zac B; 16.06.2016