检测 PHP 代码块的超时

发布于 2024-12-05 12:25:12 字数 592 浏览 1 评论 0原文

如果 PHP 中的代码块花费太长时间,有没有办法可以中止该代码块?也许是这样的:

//Set the max time to 2 seconds
$time = new TimeOut(2);
$time->startTime();

sleep(3)

$time->endTime();
if ($time->timeExpired()){
    echo 'This function took too long to execute and was aborted.';
} 

它不必完全像上面那样,但是是否有任何本地 PHP 函数或类可以执行类似的操作?

编辑:Ben Lee 使用 pcnt_fork 的回答将是完美的解决方案,只是它不适用于 Windows。有没有其他方法可以使用适用于 Windows 和 Linux 的 PHP 来完成此任务,但不需要外部库?

编辑2:XzKto的解决方案在某些情况下有效,但不一致,而且无论我尝试什么,我似乎都无法捕获异常。该用例正在检测单元测试的超时。如果测试超时,我想终止它,然后继续下一个测试。

Is there a way you can abort a block of code if it's taking too long in PHP? Perhaps something like:

//Set the max time to 2 seconds
$time = new TimeOut(2);
$time->startTime();

sleep(3)

$time->endTime();
if ($time->timeExpired()){
    echo 'This function took too long to execute and was aborted.';
} 

It doesn't have to be exactly like above, but are there any native PHP functions or classes that do something like this?

Edit: Ben Lee's answer with pcnt_fork would be the perfect solution except that it's not available for Windows. Is there any other way to accomplish this with PHP that works for Windows and Linux, but doesn't require an external library?

Edit 2: XzKto's solution works in some cases, but not consistently and I can't seem to catch the exception, no matter what I try. The use case is detecting a timeout for a unit test. If the test times out, I want to terminate it and then move on to the next test.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(9

等数载,海棠开 2024-12-12 12:25:12

您可以通过分叉进程,然后使用父进程来监视子进程来实现此目的。 pcntl_fork 是一种分叉进程的方法,所以你有两个近内存中的相同程序并行运行。唯一的区别是,在一个进程中,父进程 pcntl_fork 返回一个正整数,该整数对应于子进程的进程 ID。在另一个进程中,子进程 pcntl_fork 返回 0。

这是一个示例:

$pid = pcntl_fork();
if ($pid == 0) {
    // this is the child process
} else {
    // this is the parent process, and we know the child process id is in $pid
}

这是基本结构。下一步是添加流程到期时间。你的东西将在子进程中运行,父进程将只负责监视和计时子进程。但是为了让一个进程(父进程)杀死另一个进程(子进程),需要有一个信号。信号是进程通信的方式,表示“你应该立即结束”的信号是SIGKILL。您可以使用 posix_kill 发送此信号。所以父进程应该等待 2 秒然后杀死子进程,如下所示:

$pid = pcntl_fork();
if ($pid == 0) {
    // this is the child process
    // run your potentially time-consuming method
} else {
    // this is the parent process, and we know the child process id is in $pid
    sleep(2); // wait 2 seconds
    posix_kill($pid, SIGKILL); // then kill the child
}

You can do this by forking the process, and then using the parent process to monitor the child process. pcntl_fork is a method that forks the process, so you have two nearly identical programs in memory running in parallel. The only difference is that in one process, the parent, pcntl_fork returns a positive integer which corresponds to the process id of the child process. And in the other process, the child, pcntl_fork returns 0.

Here's an example:

$pid = pcntl_fork();
if ($pid == 0) {
    // this is the child process
} else {
    // this is the parent process, and we know the child process id is in $pid
}

That's the basic structure. Next step is to add a process expiration. Your stuff will run in the child process, and the parent process will be responsible only for monitoring and timing the child process. But in order for one process (the parent) to kill another (the child), there needs to be a signal. Signals are how processes communicate, and the signal that means "you should end immediately" is SIGKILL. You can send this signal using posix_kill. So the parent should just wait 2 seconds then kill the child, like so:

$pid = pcntl_fork();
if ($pid == 0) {
    // this is the child process
    // run your potentially time-consuming method
} else {
    // this is the parent process, and we know the child process id is in $pid
    sleep(2); // wait 2 seconds
    posix_kill($pid, SIGKILL); // then kill the child
}
天气好吗我好吗 2024-12-12 12:25:12

如果除了分叉之外,如果您在一个命令(例如 sleep())上编写暂停脚本,那么您实际上无法做到这一点,但是对于特殊情况,有很多解决方法:如果您在数据库查询上编程暂停,则像异步查询一样,如果您在数据库查询上暂停,则可以使用 proc_open程序在某些外部执行等处暂停。不幸的是,它们都是不同的,因此没有通用的解决方案。

如果你的脚本等待一个很长的循环/很多行代码,你可以做一个像这样的肮脏的把戏:

declare(ticks=1);

class Timouter {

    private static $start_time = false,
    $timeout;

    public static function start($timeout) {
        self::$start_time = microtime(true);
        self::$timeout = (float) $timeout;
        register_tick_function(array('Timouter', 'tick'));
    }

    public static function end() {
        unregister_tick_function(array('Timouter', 'tick'));
    }

    public static function tick() {
        if ((microtime(true) - self::$start_time) > self::$timeout)
            throw new Exception;
    }

}

//Main code
try {
    //Start timeout
    Timouter::start(3);

    //Some long code to execute that you want to set timeout for.
    while (1);
} catch (Exception $e) {
    Timouter::end();
    echo "Timeouted!";
}

但我不认为它很好。如果您具体说明具体情况,我想我们可以更好地帮助您。

You can't really do that if you script pauses on one command (for example sleep()) besides forking, but there are a lot of work arounds for special cases: like asynchronous queries if you programm pauses on DB query, proc_open if you programm pauses at some external execution etc. Unfortunately they are all different so there is no general solution.

If you script waits for a long loop/many lines of code you can do a dirty trick like this:

declare(ticks=1);

class Timouter {

    private static $start_time = false,
    $timeout;

    public static function start($timeout) {
        self::$start_time = microtime(true);
        self::$timeout = (float) $timeout;
        register_tick_function(array('Timouter', 'tick'));
    }

    public static function end() {
        unregister_tick_function(array('Timouter', 'tick'));
    }

    public static function tick() {
        if ((microtime(true) - self::$start_time) > self::$timeout)
            throw new Exception;
    }

}

//Main code
try {
    //Start timeout
    Timouter::start(3);

    //Some long code to execute that you want to set timeout for.
    while (1);
} catch (Exception $e) {
    Timouter::end();
    echo "Timeouted!";
}

but I don't think it is very good. If you specify the exact case I think we can help you better.

小伙你站住 2024-12-12 12:25:12

这是一个老问题,现在可能已经解决了很多次,但是对于寻找简单方法来解决这个问题的人来说,现在有一个库: PHP 调用程序

This is an old question, and has probably been solved many times by now, but for people looking for an easy way to solve this problem, there is a library now: PHP Invoker.

惯饮孤独 2024-12-12 12:25:12

如果执行时间超过限制,可以使用声明函数。 http://www.php.net/manual/en/control-structs .declare.php

这里有一个如何使用的代码示例

define("MAX_EXECUTION_TIME", 2); # seconds

$timeline = time() + MAX_EXECUTION_TIME;

function check_timeout()
{
    if( time() < $GLOBALS['timeline'] ) return;
    # timeout reached:
    print "Timeout!".PHP_EOL;
    exit;
}

register_tick_function("check_timeout");
$data = "";

declare( ticks=1 ){
    # here the process that might require long execution time
    sleep(5); // Comment this line to see this data text
    $data = "Long process result".PHP_EOL;
}

# Ok, process completed, output the result:
print $data;

通过此代码,您将看到超时消息。
如果您想在声明块内获得长处理结果,您只需删除 sleep(5) 行或增加在脚本开头声明的最大执行时间

You can use declare function if the execution time exceeds the limits. http://www.php.net/manual/en/control-structures.declare.php

Here a code example of how to use

define("MAX_EXECUTION_TIME", 2); # seconds

$timeline = time() + MAX_EXECUTION_TIME;

function check_timeout()
{
    if( time() < $GLOBALS['timeline'] ) return;
    # timeout reached:
    print "Timeout!".PHP_EOL;
    exit;
}

register_tick_function("check_timeout");
$data = "";

declare( ticks=1 ){
    # here the process that might require long execution time
    sleep(5); // Comment this line to see this data text
    $data = "Long process result".PHP_EOL;
}

# Ok, process completed, output the result:
print $data;

With this code you will see the timeout message.
If you want to get the Long process result inside the declare block you can just remove the sleep(5) line or increase the Max Execution Time declared at the start of the script

拥抱我好吗 2024-12-12 12:25:12

如果您不在其中,那么 set-time-limit 怎么样?安全模式。

What about set-time-limit if you are not in the safe mode.

同展鸳鸯锦 2024-12-12 12:25:12

大约两分钟就做好了,我忘了调用 $time->startTime(); 所以我真的不知道到底花了多长时间;)

class TimeOut{
    public function __construct($time=0)
    {
        $this->limit = $time;
    }

    public function startTime()
    {
        $this->old = microtime(true);
    }

    public function checkTime()
    {
        $this->new = microtime(true);
    }

    public function timeExpired()
    {
        $this->checkTime();
        return ($this->new - $this->old > $this->limit);
    }

}

演示

我不太明白您的 endTime() 调用的作用,因此我改为使用 checkTime() ,这也没有真正的目的,只是更新内部值。 timeExpired() 会自动调用它,因为如果您忘记调用 checkTime() 并且它使用的是旧时间,那么它肯定会很糟糕。

Cooked this up in about two minutes, I forgot to call $time->startTime(); so I don't really know exactly how long it took ;)

class TimeOut{
    public function __construct($time=0)
    {
        $this->limit = $time;
    }

    public function startTime()
    {
        $this->old = microtime(true);
    }

    public function checkTime()
    {
        $this->new = microtime(true);
    }

    public function timeExpired()
    {
        $this->checkTime();
        return ($this->new - $this->old > $this->limit);
    }

}

And the demo.

I don't really get what your endTime() call does, so I made checkTime() instead, which also serves no real purpose but to update the internal values. timeExpired() calls it automatically because it would sure stink if you forgot to call checkTime() and it was using the old times.

孤蝉 2024-12-12 12:25:12

您还可以使用第二个脚本,其中包含暂停代码,该代码通过设置了超时的curl 调用执行。另一个明显的解决方案是解决暂停的原因。

You can also use a 2nd script that has the pause code in it that is executed via a curl call with a timeout set. The other obvious solution is to fix the cause of the pause.

蓝眼泪 2024-12-12 12:25:12

这是我的方法。感谢其他人的回答:

<?php
class Timeouter
{
   private static $start_time = FALSE, $timeout;

   /**
    * @param   integer $seconds Time in seconds
    * @param null      $error_msg
    */
   public static function limit($seconds, $error_msg = NULL)
   : void
   {
      self::$start_time = microtime(TRUE);
      self::$timeout    = (float) $seconds;
      register_tick_function([ self::class, 'tick' ], $error_msg);
   }

   public static function end()
   : void
   {
      unregister_tick_function([ self::class, 'tick' ]);
   }

   public static function tick($error)
   : void
   {
      if ((microtime(TRUE) - self::$start_time) > self::$timeout) {
         throw new \RuntimeException($error ?? 'You code took too much time.');
      }
   }

   public static function step()
   : void
   {
      usleep(1);
   }
}

那你可以尝试这样:

  <?php
  try {
     //Start timeout
     Timeouter::limit(2, 'You code is heavy. Sorry.');

     //Some long code to execute that you want to set timeout for.
     declare(ticks=1) {
        foreach (range(1, 100000) as $x) {
           Timeouter::step(); // Not always necessary
           echo $x . "-";
        }
     }

     Timeouter::end();
  } catch (Exception $e) {
     Timeouter::end();
     echo $e->getMessage(); // 'You code is heavy. Sorry.'
  }

Here is my way to do that. Thanks to others answers:

<?php
class Timeouter
{
   private static $start_time = FALSE, $timeout;

   /**
    * @param   integer $seconds Time in seconds
    * @param null      $error_msg
    */
   public static function limit($seconds, $error_msg = NULL)
   : void
   {
      self::$start_time = microtime(TRUE);
      self::$timeout    = (float) $seconds;
      register_tick_function([ self::class, 'tick' ], $error_msg);
   }

   public static function end()
   : void
   {
      unregister_tick_function([ self::class, 'tick' ]);
   }

   public static function tick($error)
   : void
   {
      if ((microtime(TRUE) - self::$start_time) > self::$timeout) {
         throw new \RuntimeException($error ?? 'You code took too much time.');
      }
   }

   public static function step()
   : void
   {
      usleep(1);
   }
}

Then you can try like this:

  <?php
  try {
     //Start timeout
     Timeouter::limit(2, 'You code is heavy. Sorry.');

     //Some long code to execute that you want to set timeout for.
     declare(ticks=1) {
        foreach (range(1, 100000) as $x) {
           Timeouter::step(); // Not always necessary
           echo $x . "-";
        }
     }

     Timeouter::end();
  } catch (Exception $e) {
     Timeouter::end();
     echo $e->getMessage(); // 'You code is heavy. Sorry.'
  }
你的背包 2024-12-12 12:25:12

我使用 pcntl_fork 和 lockfile 在 php 中编写了一个脚本来控制超时后执行终止操作的外部调用的执行。


#!/usr/bin/env php
<?php

if(count($argv)<4){
    print "\n\n\n";
    print "./fork.php PATH \"COMMAND\" TIMEOUT\n"; // TIMEOUT IN SECS
    print "Example:\n";
    print "./fork.php /root/ \"php run.php\" 20";
    print "\n\n\n";
    die;
}

$PATH = $argv[1];
$LOCKFILE = $argv[1].$argv[2].".lock";
$TIMEOUT = (int)$argv[3];
$RUN = $argv[2];

chdir($PATH);


$fp = fopen($LOCKFILE,"w"); 
    if (!flock($fp, LOCK_EX | LOCK_NB)) {
            print "Already Running\n";
            exit();
    }

$tasks = [
  "kill",
  "run",
];

function killChilds($pid,$signal) { 
    exec("ps -ef| awk '\$3 == '$pid' { print  \$2 }'", $output, $ret); 
    if($ret) return 'you need ps, grep, and awk'; 
    while(list(,$t) = each($output)) { 
            if ( $t != $pid && $t != posix_getpid()) { 
                    posix_kill($t, $signal);
            } 
    }    
} 

$pidmaster = getmypid();
print "Add PID: ".(string)$pidmaster." MASTER\n";

foreach ($tasks as $task) {
    $pid = pcntl_fork();

    $pidslave = posix_getpid();
    if($pidslave != $pidmaster){
        print "Add PID: ".(string)$pidslave." ".strtoupper($task)."\n";
    }

  if ($pid == -1) {
    exit("Error forking...\n");
  }
  else if ($pid == 0) {
    execute_task($task);        
        exit();
  }
}

while(pcntl_waitpid(0, $status) != -1);
echo "Do stuff after all parallel execution is complete.\n";
unlink($LOCKFILE);


function execute_task($task_id) {
    global $pidmaster;
    global $TIMEOUT;
    global $RUN;

    if($task_id=='kill'){
        print("SET TIMEOUT = ". (string)$TIMEOUT."\n");
        sleep($TIMEOUT);
        print("FINISHED BY TIMEOUT: ". (string)$TIMEOUT."\n");
        killChilds($pidmaster,SIGTERM);

        die;
  }elseif($task_id=='run'){
        ###############################################
        ### START EXECUTION CODE OR EXTERNAL SCRIPT ###
        ###############################################

            system($RUN);

        ################################    
        ###             END          ###
        ################################
        killChilds($pidmaster,SIGTERM);
        die;
    }
}

测试脚本run.php

<?php

$i=0;
while($i<25){
    print "test... $i\n";
    $i++;
    sleep(1);
}

I made a script in php using pcntl_fork and lockfile to control the execution of external calls doing the kill after the timeout.


#!/usr/bin/env php
<?php

if(count($argv)<4){
    print "\n\n\n";
    print "./fork.php PATH \"COMMAND\" TIMEOUT\n"; // TIMEOUT IN SECS
    print "Example:\n";
    print "./fork.php /root/ \"php run.php\" 20";
    print "\n\n\n";
    die;
}

$PATH = $argv[1];
$LOCKFILE = $argv[1].$argv[2].".lock";
$TIMEOUT = (int)$argv[3];
$RUN = $argv[2];

chdir($PATH);


$fp = fopen($LOCKFILE,"w"); 
    if (!flock($fp, LOCK_EX | LOCK_NB)) {
            print "Already Running\n";
            exit();
    }

$tasks = [
  "kill",
  "run",
];

function killChilds($pid,$signal) { 
    exec("ps -ef| awk '\$3 == '$pid' { print  \$2 }'", $output, $ret); 
    if($ret) return 'you need ps, grep, and awk'; 
    while(list(,$t) = each($output)) { 
            if ( $t != $pid && $t != posix_getpid()) { 
                    posix_kill($t, $signal);
            } 
    }    
} 

$pidmaster = getmypid();
print "Add PID: ".(string)$pidmaster." MASTER\n";

foreach ($tasks as $task) {
    $pid = pcntl_fork();

    $pidslave = posix_getpid();
    if($pidslave != $pidmaster){
        print "Add PID: ".(string)$pidslave." ".strtoupper($task)."\n";
    }

  if ($pid == -1) {
    exit("Error forking...\n");
  }
  else if ($pid == 0) {
    execute_task($task);        
        exit();
  }
}

while(pcntl_waitpid(0, $status) != -1);
echo "Do stuff after all parallel execution is complete.\n";
unlink($LOCKFILE);


function execute_task($task_id) {
    global $pidmaster;
    global $TIMEOUT;
    global $RUN;

    if($task_id=='kill'){
        print("SET TIMEOUT = ". (string)$TIMEOUT."\n");
        sleep($TIMEOUT);
        print("FINISHED BY TIMEOUT: ". (string)$TIMEOUT."\n");
        killChilds($pidmaster,SIGTERM);

        die;
  }elseif($task_id=='run'){
        ###############################################
        ### START EXECUTION CODE OR EXTERNAL SCRIPT ###
        ###############################################

            system($RUN);

        ################################    
        ###             END          ###
        ################################
        killChilds($pidmaster,SIGTERM);
        die;
    }
}

Test Script run.php

<?php

$i=0;
while($i<25){
    print "test... $i\n";
    $i++;
    sleep(1);
}
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文