純PHP實現定時器任務(Timer)


基礎知識

  此程序在Linux下開發,以cli模式運行,一下是基本知識的簡要介紹。

  • CLI:PHP的命令行模式,常見的WEB應用使用的是fpm;
  • 進程:進程是程序運行的基本單元,進程之間是獨立運行且互不干擾的,有獨立的運行空間,每個進程都有一個進程控制塊;
  • 進程間通信:既然進程是獨立運行,我們需要一種機制保證不同進程信息的交換,進程間通信主要包括:管道,IPC(共享內存,信號,消息隊列),套接字;
  • PCNTL擴展:PHP的一個進程擴展,主要用到pcntl_alarm()函數,詳細介紹請查閱官網.
  • 實現原理    

      用一個三維數組保存所有需要執行的任務,一級索引為時間戳,值為執行任務的方法、回調參數等,具體數組形式如下:

  • array(
            '1438156396' => array(
                    array(1,array('Class','Func'), array(), true),  
            )
    )
    說明:
    時間戳
    array(1,array('Class','Func'), array(), true) 
    參數依次表示: 執行時間間隔,回調函數,傳遞給回調函數的參數,是否持久化(ture則一直保存在數據中,否則執行一次后刪除)

     

這些任務可以是任意類的方法。既然是定時任務,我們需要一個類似計時的東東,此方案采用信號量去做,每一秒向當前進程發送SIGALRM信號,並捕獲該信號,觸發信號處理函數,循環遍歷數據,判斷是否有當前時間需要執行的任務。如果有則采用回調方式觸發,並把參數傳遞給該方法。

<?php
/**
*定時器
*/
class Timer
{
    //保存所有定時任務
    public static $task = array();

        //定時間隔
        public static $time = 1;

        /**
    *開啟服務
        *@param $time int
       */
    public static function run($time = null)
        {
        if($time)
            {
                    self::$time = $time;
            }
            self::installHandler();
            pcntl_alarm(1);
         }
        /**
        *注冊信號處理函數
        */
        public static function installHandler()
        {
            pcntl_signal(SIGALRM, array('Timer','signalHandler'));
        }

        /**
        *信號處理函數
        */
        public static function signalHandler()
        {
            self::task();
        //一次信號事件執行完成后,再觸發下一次
        pcntl_alarm(self::$time);
        }

        /**
        *執行回調
        */
        public static function task()
        {
            if(empty(self::$task))
            {//沒有任務,返回
                    return ;
            }
            foreach(self::$task as $time => $arr)
        {
                    $current = time();
        
                foreach($arr as $k => $job)
            {//遍歷每一個任務
                        $func = $job['func'];    /*回調函數*/
                        $argv = $job['argv'];    /*回調函數參數*/
                $interval = $job['interval'];    /*時間間隔*/
                        $persist = $job['persist'];    /*持久化*/

                        if($current == $time)
                        {//當前時間有執行任務

                    //調用回調函數,並傳遞參數
                               call_user_func_array($func, $argv);
                    
                    //刪除該任務
                            unset(self::$task[$time][$k]);
                        }
                        if($persist)
                        {//如果做持久化,則寫入數組,等待下次喚醒
                               self::$task[$current+$interval][] = $job;
                        }
            }
            if(empty(self::$task[$time]))
            {
                unset(self::$task[$time]);
            }
            }
        }

        /**
        *添加任務
        */
        public static function add($interval, $func, $argv = array(), $persist = false)
        {
            if(is_null($interval))
            {
                return;
            }
            $time = time()+$interval;
        //寫入定時任務
        self::$task[$time][] = array('func'=>$func, 'argv'=>$argv, 'interval'=>$interval, 'persist'=>$persist);
        }

        /**
        *刪除所有定時器任務
        */
        public function dellAll()
        {
            self::$task = array();
        }
}

這是定時器類核心部分,有一個靜態變量保存有所有需要執行的任務,這里為什么是靜態的呢?大家自行思考.當進程接受到 SIGALRM 信號后,觸發 signalHandler 函數,隨后循序遍歷數組查看是否有當前時間需要執行的任務,有則回調,並傳遞參數,刪除當前job,隨后檢查是否要做持久化任務,是則繼續將當前job寫入事件數組等待下次觸發,最后再為當前進程設置一個鬧鍾信號.可以看出這個定時器,只要觸發一次就會從內部再次觸發,得到自循環目的.

<?php

class DoJob
{
    public function job( $param = array() )
    {
        $time = time();
        echo "Time: {$time}, Func: ".get_class()."::".__FUNCTION__."(".json_encode($param).")\n";
    }
}

這是回調類及函數,為方便說明,加入不少調試信息.Timer類及回調都有了,我們看看使用場景是怎么樣的.

<?php

require_once(__DIR__."/Timer.php");
require_once(__DIR__."/DoJob.php");


Timer::dellAll();

Timer::add( 1, array('DoJob','job'), array(),true);

Timer::add( 3, array('DoJob','job'),array('a'=>1), false);

echo "Time start: ".time()."\n";
Timer::run();

while(1)
{
    sleep(1);
    pcntl_signal_dispatch();
}

代碼非常短,這里注冊了兩個job,隨后運行定時器,在一個無限循環里捕捉信號觸發動作,如果不捕獲將無法觸發事先注冊的處理函數.這樣一個自循環的定時器開發完成.運行結果如下:

 

 如我們場景類添加的任務一樣,在90的時候執行了兩個任務,一個為持久化的不帶參數的job,一個為非持久化帶參數的job,隨后非持久化job不再執行.

總結

  • 在收到信號前,當前進程不能退出.這里我使用了條件永遠為真的循環.在我們實際生產環境中,需要創造這么一個先決條件,比如說,我們有一組服務,這些服務都是一直運行的,不管是IO訪問,等待socket鏈接等等,當前服務都不會終止,即使進程阻塞也不會有問題,這種場景,也就是有一個一直運行的服務中使用.
  • 目前PHP只支持以秒為單位的觸發,不支持更小時間單位,對位定時任務而言基本足夠

 

 

 服務器定時任務

Unix平台

如果您使用 Unix 系統,您需要在您的 PHP 腳本的最前面加上一行特殊的代碼,使得它能夠被執行,這樣系統就能知道用什么樣的程序要運行該腳本。為 Unix 系統增加的第一行代碼不會影響該腳本在 Windows 下的運行,因此您也可以用該方法編寫跨平台的腳本程序。

1、在Crontab中使用PHP執行腳本

就像在Crontab中調用普通的shell腳本一樣(具體Crontab用法),使用PHP程序來調用PHP腳本,每一小時執行 myscript.php 如下:

# crontab -e 00 * * * * /usr/local/bin/php /home/john/myscript.php

/usr/local/bin/php為PHP程序的路徑。

2、在Crontab中使用URL執行腳本

如果你的PHP腳本可以通過URL觸發,你可以使用 lynx 或 curl 或 wget 來配置你的Crontab。

下面的例子是使用Lynx文本瀏覽器訪問URL來每小時執行PHP腳本。Lynx文本瀏覽器默認使用對話方式打開URL。但是,像下面的,我們在lynx命令行中使用-dump選項來把URL的輸出轉換來標准輸出。

00 * * * * lynx -dump http://www.sf.net/myscript.php

下面的例子是使用 CURL 訪問URL來每5分執行PHP腳本。Curl默認在標准輸出顯示輸出。使用 "curl -o" 選項,你也可以把腳本的輸出轉儲到臨時文件temp.txt。

*/5 * * * * /usr/bin/curl -o temp.txt http://www.sf.net/myscript.php

下面的例子是使用WGET訪問URL來每10分執行PHP腳本。-q 選項表示安靜模式。"-O temp.txt" 表示輸出會發送到臨時文件。

*/10 * * * * /usr/bin/wget -q -O temp.txt http://www.sf.net/myscript.php

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM