PHP-多任務(wù)秒級定時器

描述

最近在公司部署crontab的時候,突發(fā)奇想是否可以用PHP去實(shí)現(xiàn)一個定時器,顆粒度到秒級就好,因?yàn)閏rontab最多到分鐘級別,同時也調(diào)研了一下用PHP去實(shí)現(xiàn)的定時器還真不太多,Swoole 擴(kuò)展里面到實(shí)現(xiàn)了一個毫秒級的定時器很高效,但畢竟不是純PHP代碼寫的,所以最后還是考慮用PHP去實(shí)現(xiàn)一個定時器類,以供學(xué)習(xí)參考。

實(shí)現(xiàn)

在實(shí)現(xiàn)定時器代碼的時候,用到了PHP系統(tǒng)自帶的兩個擴(kuò)展

Pcntl - 多進(jìn)程擴(kuò)展 :

主要就是讓PHP可以同時開啟很多子進(jìn)程,并行的去處理一些任務(wù)。

Spl - SplMinHeap - 小頂堆

一個小頂堆數(shù)據(jù)結(jié)構(gòu),在實(shí)現(xiàn)定時器的時候,采用這種結(jié)構(gòu)效率還是不錯的,插入、刪除的時間復(fù)雜度都是 O(logN) ,像 libevent 的定時器也在 1.4 版本以后采用了這種數(shù)據(jù)結(jié)構(gòu)之前用的是 rbtree,如果要是使用鏈表或者固定的數(shù)組,每次插入、刪除可能都需要重新遍歷或者排序,還是有一定的性能問題的。

流程

大致流程

說明

1、定義定時器結(jié)構(gòu),有什么參數(shù)之類的.
2、然后全部注冊進(jìn)我們的定時器類 Timer.
3、調(diào)用定時器類的monitor方法,開始進(jìn)行監(jiān)聽.
4、監(jiān)聽過程就是一個while死循環(huán),不斷的去看時間堆的堆頂是否到期了,本來考慮每秒循環(huán)看一次,后來一想每秒循環(huán)看一次還是有點(diǎn)問題,如果正好在我們sleep(1)的時候定時器有到期的了,那我們就不能馬上去精準(zhǔn)執(zhí)行,可能會有延時的風(fēng)險(xiǎn),所以還是采用 usleep(1000) 毫秒級的去看并且也可以將進(jìn)程掛起減輕 CPU 負(fù)載.

代碼

 /***
 * Class Timer
 */
 class Timer extends SplMinHeap
 {
     /**
      * 比較父節(jié)點(diǎn)和新插入節(jié)點(diǎn)大小
      * @param mixed $value1
      * @param mixed $value2
      * @return int
      */
     protected function compare($value1, $value2)
     {
         if ($value1['timeout'] > $value2['timeout']) {
             return -1;
         }
         if ($value1['timeout'] < $value2['timeout']) {
             return 1;
         }
         return 0;
     }

     /**
      * 插入節(jié)點(diǎn)
      * @param mixed $value
      */
     public function insert($value)
     {
         $value['timeout'] = time() + $value['expire'];
         parent::insert($value);
     }

     /**
      * 監(jiān)聽
      * @param bool $debug
      */
     public function monitor($debug = false)
     {
         while (!$this->isEmpty()) {
             $this->exec($debug);
             usleep(1000);
         }
     }

     /**
      * 執(zhí)行
      * @param $debug
      */
     private function exec($debug)
     {
         $hit  = 0;
         $t1   = microtime(true);
         while (!$this->isEmpty()) {
             $node = $this->top();
             if ($node['timeout'] <= time()) {
                 //出堆或入堆
                 $node['repeat'] ? $this->insert($this->extract()) : $this->extract();
                 $hit = 1;
                 //開啟子進(jìn)程
                 if (pcntl_fork() == 0) {
                     empty($node['action']) ? '' : call_user_func($node['action']);
                     exit(0);
                 }
                 //忽略子進(jìn)程,子進(jìn)程退出由系統(tǒng)回收
                 pcntl_signal(SIGCLD, SIG_IGN);
             } else {
                 break;
             }
         }
         $t2 = microtime(true);
         echo ($debug && $hit) ? '時間堆 - 調(diào)整耗時: ' . round($t2 - $t1, 3) . "秒\r\n" : '';
     }
 }

實(shí)例

$timer = new Timer();

//注冊 - 3s - 重復(fù)觸發(fā)
$timer->insert(array('expire' => 3, 'repeat' => true, 'action' => function(){
    echo '3秒 - 重復(fù) - hello world' . "\r\n";
}));

//注冊 - 3s - 重復(fù)觸發(fā)
$timer->insert(array('expire' => 3, 'repeat' => true, 'action' => function(){
    echo '3秒 - 重復(fù) - gogo' . "\r\n";
}));

//注冊 - 6s - 觸發(fā)一次
$timer->insert(array('expire' => 6, 'repeat' => false, 'action' => function(){
    echo '6秒 - 一次 - hello xxxx' . "\r\n";
}));

//監(jiān)聽
$timer->monitor(false);
執(zhí)行結(jié)果

也測試過比較極端的情況,同時1000個定時器1s全部到期,時間堆全部調(diào)整完僅需 0.126s 這是沒問題的,但是每調(diào)整完一個定時器就需要去開啟一個子進(jìn)程,這塊可能比較耗時了,有可能1s處理不完這1000個,就會影響下次監(jiān)聽繼續(xù)觸發(fā),但是不開啟子進(jìn)程,比如直接執(zhí)行應(yīng)該還是可以處理完的。。。。當(dāng)然肯定有更好的方法,目前只能想到這樣。

結(jié)束

以上僅供學(xué)習(xí)參考,有問題請及時指正,共同學(xué)習(xí)??!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
禁止轉(zhuǎn)載,如需轉(zhuǎn)載請通過簡信或評論聯(lián)系作者。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容