Почему не выполняются все процессы оболочки в моих обещаниях (стартовые блоки)? (Это ошибка?)

#raku

#raku

Вопрос:

Я хочу запустить несколько процессов оболочки, но когда я пытаюсь запустить более 63, они зависают. Когда я уменьшаю max_threads в пуле потоков до n , он зависает после выполнения n команды оболочки.

Как вы можете видеть в приведенном ниже коде, проблема не в start блоках как таковых, а в start блоках, которые содержат shell команду:

 #!/bin/env perl6
my $*SCHEDULER = ThreadPoolScheduler.new( max_threads => 2 );

my @processes;

# The Promises generated by this loop work as expected when awaited
for @*ARGS -> $item {
    @processes.append(
        start { say "Planning on processing $item" }
    );
}

# The nth Promise generated by the following loop hangs when awaited (where n = max_thread)
for @*ARGS -> $item {
    @processes.append(
        start { shell "echo 'processing $item'" }
    );
}
await(@processes);
  

Запуск ./process_items foo bar baz выдает следующий вывод, зависающий после processing bar , который происходит сразу после того, как n й (здесь 2 nd) поток запустился с использованием shell :

 Planning on processing foo
Planning on processing bar
Planning on processing baz
processing foo
processing bar
  

Что я делаю не так? Или это ошибка?

Дистрибутивы Perl 6, протестированные на CentOS 7:
Rakudo Star 2018.06
, Rakudo Star 2018.10
, Rakudo Star 2019.03-RC2
, Rakudo Star 2019.03

С Rakudo Star 2019.03-RC2 use v6.c versus use v6.d не имело никакого значения.

Комментарии:

1. Запуск блока цикла start {say "Starting $item"; loop (my $i=0; $i<100000; $i ) {state $a ;} } запускает максимальное количество потоков в пуле. Если заданий больше, чем потоков, остальные ожидают свободного потока, а затем выполняются, как и ожидалось. На мой взгляд, ваш вопрос указывает на ошибку.

2. @drclaw Спасибо! Я обновил свой вопрос, чтобы попытаться подчеркнуть разницу между тем, когда это работает (как вы демонстрируете) и когда это кажется «глючным».

Ответ №1:

Подлодки shell и run используют Proc , что реализовано в терминах Proc::Async . При этом используется пул потоков внутри. При заполнении пула блокирующими вызовами shell пул потоков исчерпывается и поэтому не может обрабатывать события, что приводит к зависанию.

Было бы намного лучше использовать Proc::Async непосредственно для этой задачи. Подход с использованием shell и загрузкой реальных потоков не будет хорошо масштабироваться; у каждого потока ОС есть издержки памяти, издержки GC и так далее. Поскольку создание множества дочерних процессов не привязано к процессору, это довольно расточительно; на самом деле, требуется всего один или два реальных потока. Таким образом, в данном случае, возможно, реализация, отталкивающая вас при выполнении чего-то неэффективного, не самое худшее.

Я замечаю, что одной из причин использования shell и пула потоков является попытка ограничить количество одновременных процессов. Но это не очень надежный способ сделать это; просто потому, что текущая реализация пула потоков устанавливает по умолчанию максимум в 64 потока, не означает, что это всегда будет так.

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

 my $degree = 4;
my @tests = dir('t').grep(/.t$/);
react {
    sub run-one {
        my $test = @tests.shift // return;
        my $proc = Proc::Async.new('perl6', '-Ilib', $test);
        my @output = "FILE: $test";
        whenever $proc.stdout.lines {
            push @output, "OUT: $_";
        }
        whenever $proc.stderr.lines {
            push @output, "ERR: $_";
        }
        my $finished = $proc.start;
        whenever $finished {
            push @output, "EXIT: {.exitcode}";
            say @output.join("n");
            run-one();
        }
    }
    run-one for 1..$degree;
}
  

Ключевым моментом здесь является вызов run-one при завершении процесса, что означает, что вы всегда заменяете завершенный процесс новым, поддерживая — пока есть чем заняться — до 4 процессов, запущенных одновременно. react Блок, естественно, заканчивается, когда все процессы завершены, из-за того, что количество событий, на которые подписаны, падает до нуля.