Вы неверно используете 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

watcher screenshot

Эта программа не делает ничего полезного. Давайте изобразим тяжелую работу.

<?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);
    }
}

watcher screenshot

Ууупс. Наш мастер процесс завершился раньше чем дочерний и мы потеряли контроль над ним. Чтобы избежать этого воспользуемся функцией 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);. Запускаем и пробуем.

watcher screenshot watcher screenshot

Отлично! То что нам и нужно!

Заключение

Если у вас есть большая задача которую можно распараллелить, то смело используйте форки, правильно обрабатывайте сигналы и не забудьте включить их асинхронную обработку.

Как я и обещал в конце почему же вы не правильно используете sleep?

Как вы думаете, может ли проспать скрипт <100 секунд?

<?php

pcntl_signal(15, function () {
    echo "Got signal 15 \n";
});

$startSleep = time();

sleep(100);

$slept = time() - $startSleep;
echo "Slept: {$slept} seconds!\n"; 

watcher screenshot

Неожиданно верно? На самом деле я послал 15 сигнал скрипту и sleep прервался вернув количество секунд которое он недоспал. Причём это описано в документации.

Так как лучше нам написать чтобы скрипт продолжил спать?

<?php

pcntl_signal(15, function () {
    echo "Got signal 15 \n";
});

$sleep = 100;
while ($sleep) {
    echo "Estimate sleep {$sleep} \n";
    $sleep = sleep($sleep);
}