有没有办法用比 pcntl_alarm(int $seconds) 更细粒度的延迟来发送 SIGALRM 信号?

Is there a way to signal a SIGALRM with a finer-grained delay than pcntl_alarm(int $seconds)?

pcntl_alarm(int $seconds) 只有秒的分辨率。在 PHP 中有没有办法以毫秒的延迟发出 SIGALRM 信号?也许 posix_kill() 带有延迟参数?

PS.: 我知道 Swoole\Process::alarm() 来自 PECL 扩展 Swoole,但我正在寻找更简单的 PHP 解决方案。

我找到了一种方法,但有点复杂:

<?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 ALARM\n";
      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);
}

...这太耗费资源了。而且因为它每次都需要生成一个新进程,所以可能也不是很 time-accurate。


附录:
我设法通过只创建一个 long-running 个中断进程来提高效率,而不是为每个中断请求创建 short-running 个进程。

它离理想还有很远的距离,但它现在可以完成工作了:

<?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 @ $time\n";
        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);
}