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

基礎知識php

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

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

      用一個三維數組保存全部須要執行的任務,一級索引爲時間戳,值爲執行任務的方法、回調參數等,具體數組形式以下:數組

  • 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寫入事件數組等待下次觸發,最後再爲當前進程設置一個鬧鐘信號.能夠看出這個定時器,只要觸發一次就會從內部再次觸發,獲得自循環目的.ruby

<?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,隨後運行定時器,在一個無限循環裏捕捉信號觸發動做,若是不捕獲將沒法觸發事先註冊的處理函數.這樣一個自循環的定時器開發完成.運行結果以下:markdown

 

 如咱們場景類添加的任務同樣,在90的時候執行了兩個任務,一個爲持久化的不帶參數的job,一個爲非持久化帶參數的job,隨後非持久化job再也不執行.curl

總結socket

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

 

 

 服務器定時任務

Unix平臺

若是您使用 Unix 系統,您須要在您的 PHP 腳本的最前面加上一行特殊的代碼,使得它可以被執行,這樣系統就能知道用什麼樣的程序要運行該腳本。爲 Unix 系統增長的第一行代碼不會影響該腳本在 Windows 下的運行,所以您也能夠用該方法編寫跨平臺的腳本程序。

一、在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程序的路徑。

二、在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
相關文章
相關標籤/搜索