pcntl_alarm(int $seconds)
only has a resolution of seconds. Is there a way in PHP to signal a SIGALRM with a delay of, say, milliseconds? Maybe a posix_kill()
with a delay argument?
PS.: I’m aware of SwooleProcess::alarm()
from the PECL extension Swoole, but I’m looking for a more bare-bones PHP solution.
Advertisement
Answer
I found one way to do it, but it is a bit convoluted:
<?php // alarm uses proc_open() to signal this process from a child process function alarm(int $msec): void { $desc = [ ['pipe', 'r'] ]; $pid = posix_getpid(); $process = proc_open('php', $desc, $pipes); fwrite( $pipes[0], "<?php usleep($msec * 1000); posix_kill($pid, SIGALRM); " ); fclose($pipes[0]); } function handleSignal(int $signal): void { switch($signal) { case SIGALRM: echo "interrupted by ALARMn"; break; } } pcntl_async_signals(true); pcntl_signal(SIGALRM, 'handleSignal'); // set alarm 200ms from now alarm(200); while(true) { echo "going to sleep for 10 seconds...n"; // first sleep(10) will be interrupted after 200ms sleep(10); }
…and it’s way too resource intensive. And because it needs to spawn a new process each time probably not very time-accurate either.
Addendum:
I’ve managed to make it more efficient by creating only one long-running interrupter process, instead of creating short-running processes for each interruption request.
It’s still far from ideal, but it does the job for now:
<?php // long-running interrupter process for the Interrupter class // that accepts interruption requests with a delay class InterrupterProcess { private $process; private $writePipe; private const PROCESS_CODE = <<<'CODE' <?php $readPipe = fopen('php://fd/3', 'r'); $interrupts = []; while(true) { $r = [$readPipe]; $w = null; $e = null; $time = microtime(true); $minExpiry = min($interrupts + [($time + 1)]); $timeout = $minExpiry - $time; if(stream_select($r, $w, $e, (int) $timeout, (int) (fmod($timeout, 1) * 1e6)) > 0) { $interrupt = json_decode(fread($readPipe, 1024), true); $interrupts[$interrupt['pid']] = $interrupt['microtime']; } $time = microtime(true); foreach($interrupts as $pid => $interrupt) { if($interrupt <= $time) { posix_kill($pid, SIGALRM); unset($interrupts[$pid]); } } } CODE; public function __construct() { $desc = [ ['pipe', 'r'], STDOUT, STDOUT, ['pipe', 'r'] ]; $this->process = proc_open(['php'], $desc, $pipes); $this->writePipe = $pipes[3]; fwrite($pipes[0], self::PROCESS_CODE); fclose($pipes[0]); } public function __destruct() { $this->destroy(); } public function setInterrupt(int $pid, float $delay): bool { if(!is_null($this->writePipe)) { fwrite($this->writePipe, json_encode(['pid' => $pid, 'microtime' => microtime(true) + $delay])); return true; } return false; } private function destroy(): void { if(!is_null($this->writePipe)) { fclose($this->writePipe); $this->writePipe = null; } if(!is_null($this->process)) { proc_terminate($this->process); proc_close($this->process); $this->process = null; } } } // main Interrupter class class Interrupter { private $process; public function __destruct() { $this->destroy(); } public function interrupt(float $delay): void { if(is_null($this->process)) { pcntl_async_signals(true); pcntl_signal(SIGALRM, function(int $signal): void { $this->handleSignal($signal); }); $this->process = $this->createInterrupterProcess(); } $this->process->setInterrupt(posix_getpid(), $delay); } private function createInterrupterProcess(): InterrupterProcess { return new InterrupterProcess(); } private function handleSignal(int $signal): void { switch($signal) { case SIGALRM: $time = time(); echo "interrupted by ALARM @ $timen"; break; } } private function destroy(): void { $this->process = null; } } $interrupter = new Interrupter(); while(true) { $time = time(); echo "going to sleep for 10 seconds @ $time...n"; $interrupter->interrupt(2); sleep(10); }