Вы неверно используете sleep() в php! Или как правильно готовить pcntl_fork()
Процессы создаются чрезвычайно просто для этого используется системный вызов fork, который создает точную копию исходного процесса, называемого родительским, а новый процесс называется дочерним. В php эта функция называется pcntl_fork. И как же им пользоваться? Давайте разбираться.
<?phpf
$pid = pcntl_fork();
if ($pid) {
echo "Master process. Child pid is: {$pid}\n";
} else {
echo "Child process. Pid {$pid}\n";
}
Функция pcntl_fork() в случае успеха возвращает pid дочерного процесса в родительском потоке, а в дочернем будет 0. В случае сбоя, в родительском процессе будет возвращено -1
Эта программа не делает ничего полезного. Давайте изобразим тяжелую работу.
<?php
$pid = pcntl_fork();
if ($pid) {
echo "Master process. Pid {$pid}\n";
hardWork(2, "Master is working...\n");
} else {
echo "Child process. Pid {$pid}\n";
hardWork(5, "Child is working...\n");
}
function hardWork($timeToWork, $text)
{
$time = time();
while (time() - $time <= $timeToWork) {
echo $text;
usleep(500000);
}
}
Ууупс. Наш мастер процесс завершился раньше чем дочерний и мы потеряли контроль над ним. Чтобы избежать этого воспользуемся функцией pcntl_waitpid, который может ждать завершения порожденного процесса или вернуть его статус
<?php
$pid = pcntl_fork();
if ($pid) {
echo "Master process. Child pid {$pid}\n";
while (true) {
$pid = pcntl_waitpid($pid, $status, WNOHANG);
if ($pid !== 0) {
exit("Child done!\n");
}
usleep(50000);
}
} else {
echo "Child process. Pid {$pid}\n";
hardWork(2, "Child is working...\n");
}
pcntl_waitpid приостанавливает выполнения мастера, пока дочерний процесс не завершится, но хотелось бы чтобы дочерний процесс сразу же вернул управление мастеру и он продолжил работать, для этого третьм аргументом передадим WNOHANG. Второй параметр - $status, который передается по ссылке используется для получения дополнительной информации о статусе, подробнее в в мануале
Обернём это все в простенький класс
<?php
class fork
{
protected $pid;
protected $callable;
public function __construct(callable $callable)
{
$this->callable = $callable;
}
public function run()
{
$this->pid = pcntl_fork();
if ($this->pid === -1) {
throw new \Exception('Can\'t start new thread!');
}
if ($this->pid) {
echo "Starting thread with pid: {$this->pid}\n";
} else {
call_user_func($this->callable);
}
}
public function isRunning() : bool
{
$processedId = pcntl_waitpid($this->pid, $status, WNOHANG);
return $processedId === 0;
}
}
Теперь стало гораздо удобнее использовать:
$fork = new fork(function () {
hardWork(20, "Child is working...\n");
});
$fork->run();
while ($fork->isRunning()) {
usleep(50000);
}
Обработка сигналов
Что будет если мы пошлём мастер процессу SIGTERM? Правильно, он завершиться, а чайлды продолжат работу. Поэтому нам нужно добавить свой обработчик сигналов. Для этого используется функция pcntl_signal.
<?php
pcntl_async_signals(true);
class fork
{
protected $pid;
protected $callable;
public function __construct(callable $callable)
{
$this->callable = $callable;
}
public function run()
{
$this->pid = pcntl_fork();
if ($this->pid === -1) {
throw new \Exception('Can\'t start new thread!');
}
if ($this->pid) {
echo "Starting thread with pid: {$this->pid}\n";
pcntl_signal(SIGTERM, [$this, 'signalHandler']);
} else {
call_user_func($this->callable);
}
}
public function signalHandler($signal)
{
switch ($signal) {
case SIGTERM:
$this->kill($signal);
break;
}
}
public function kill($signal = SIGKILL)
{
echo "killing {$this->pid}..\n";
for ($i = 0; $i < 3; $i++) {
posix_kill($this->pid, $signal);
usleep(10000);
}
}
public function isRunning() : bool
{
$processedId = pcntl_waitpid($this->pid, $status, WNOHANG);
return $processedId === 0;
}
}
Добавилось два новых метода fork::signalHandler() и fork::kill(). Теперь мастер процесс при получении сигнала должен прибить своего чайда. И не забудьте включить асинхронную обработку сингалов pcntl_async_signals(true);. Запускаем и пробуем.
Отлично! То что нам и нужно!
Заключение
Если у вас есть большая задача которую можно распараллелить, то смело используйте форки, правильно обрабатывайте сигналы и не забудьте включить их асинхронную обработку.
Как я и обещал в конце почему же вы не правильно используете sleep?
Как вы думаете, может ли проспать скрипт <100 секунд?
<?php
pcntl_signal(15, function () {
echo "Got signal 15 \n";
});
$startSleep = time();
sleep(100);
$slept = time() - $startSleep;
echo "Slept: {$slept} seconds!\n";
Неожиданно верно? На самом деле я послал 15 сигнал скрипту и sleep прервался вернув количество секунд которое он недоспал. Причём это описано в документации.
Так как лучше нам написать чтобы скрипт продолжил спать?
<?php
pcntl_signal(15, function () {
echo "Got signal 15 \n";
});
$sleep = 100;
while ($sleep) {
echo "Estimate sleep {$sleep} \n";
$sleep = sleep($sleep);
}