Асинхронное выполнение нескольких процессов

Асинхронное выполнение нескольких процессов в Laravel

В PHP 7.x есть расширение phthread, позволяющее создать новый поток. Чтобы выполнить задачу быстрее вы можете разделить её на несколько потоков.

А если phthread нет на сервере?

Вы можете порождать множество процессов, также называемых параллельными, для выполнения сложной задачи. Например, вы хотите отправить 100 тысяч электронных писем, и лучший вариант — это использование многопоточности в PHP.

В Symfony есть компонент (библиотека) «Process Component», который вы можете использовать в своем php-проекте. В Laravel он уже включен вместе с компонентами Symfony.

Вы можете сказать, что в Laravel для этого имеются Задачи (Jobs), но у них есть проблема с тайм-аутом, и не стоит вставлять более 100 тыс. записей в таблицу задач. Кроме того, каждая задача подключает/отключает MySQL, что вообще не эффективно.

Давайте посмотри, что мы можем сделать!

Создание процесса

Предположим, вы хотите запустить фоновый процесс из контроллера — просто создайте экземпляр класса Process.

$process = new Process('php ' . base_path('artisan') . ' task:long-running-task &');

$process->setTimeout(0);

$process->run();

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

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

Важность «&»

Если вы заметили, я написал «&» прямо за командой.

‘php ‘ . base_path(‘artisan’) . ‘ task:long-running-task &’

Амперсанд играет важную роль — запускает вашу команду как фоновый процесс (как демон). Он считается отдельным процессом и запускает эту команду в фоновом режиме.

Теперь, когда контроллер завершит свою работу и отправит ответ браузеру, то это никак не повлияет на этот демон/фоновый процесс.

Ваша команда теперь сама является Main-процессом, и может порождать подпроцессы, чтобы разделить задачу на несколько потоков. Смотрите ниже.

for ($i = 0; $i < $numberOfProcess; $i++) {     

    $process = new Process('php ' . base_path('artisan') . " task {$i}");
    $process->setTimeout(0);     
    $process->disableOutput();     
    $process->start();     
    $processes[] = $process;
}

Как вы видите, я использовал метод start() для создания асинхронных подпроцессов, чтобы быть уверенным, что скрипт (основной процесс) не завершится, пока не будут выполнены задачи подпроцессов.

Как главный процесс может ждать завершения задач своих подпроцессов? Смотрите код.

for ($i = 0; $i < $numberOfProcess; $i++) { 
  ... // код, описанный выше
}

// ждём завершения вышеописанных процессов
while (count($processes)) {  
    foreach ($processes as $i => $runningProcess) {    
        // этот процесс завершен, поэтому удаляем его
        if (! $runningProcess->isRunning()) {      
            unset($processes[$i]);    
        }    
        sleep(1);
    }
}

// Выполняется после того, как выполнятся все задачи подпроцессов

Таким способом мы, в своем проекте, отправили более 100 тысяч электронных писем, менее чем за минуту с помощью SendGrid API. И всего лишь тремя подпроцессами.

Вывод

Короче говоря, ваш основной процесс не должен быть завершен или уничтожен, иначе ваши дочерние процессы также будут уничтожены.

Автор: RadicalLoop
Перевод: Алексей Широков

Наш Телеграм-канал — следите за новостями о Laravel.