lua定时器与定时任务的接口设计

时间:2022-07-18 07:52:32



在所有的服务器编程当中,定时任务永远是一个不可或缺的需求。
最直接的需求就是,每天凌晨0点0分的时候总是有一大堆的各种精力重置。
怎么来设计这个接口呢,想了几个方案:

  • 每秒触发
  • 每分钟触发
  • 每整点触发
  • 每天触发
  • 每个月触发

oh no!不靠谱啊,如果这接口真设计成这样,得有多烂,灵光一现,unix下的crontab表达式非常完美的解决了这个问题。

附上crontab表达式的语法说明如下:

lua定时器与定时任务的接口设计

crontab特殊的符号说明:

"*"代表所有的取值范围内的数字。特别要注意哦!
"/"代表每的意思,如"*/5"表示每5个单位
"-"代表从某个数字到某个数字
","分散的数字

 

crontab文件的使用示例:

30 21 * * * 表示每晚的21:30 
45 4 1,10,22 * * 表示每月1、10、22日的4 : 45
10 1 * * 6,0 表示每周六、周日的1 : 10
0,30 18-23 * * * 表示在每天18 : 00至23 : 00之间每隔30分钟
0 23 * * 6 表示每星期六的11 : 00 pm
* */1 * * * 每一小时
* 23-7/1 * * * 晚上11点到早上7点之间,每隔一小时
* 8,13 * * 1-5 从周一到周五的上午8点和下午1点
0 11 4 * mon-wed 每月的4号与每周一到周三的11点
0 4 1 jan * 一月一号的4点

 

看起来很复杂的样子,但其实够用就好,我们也不需要实现全部特性。

  • 实现一个毫秒级别的定时器Update
  • 根据这个update函数实现一个秒级别定时器
  • 然后每秒取得自然时间与表达式中 分、时、几号、月份、星期几 分别匹配就可以实现了
  • 由于定时器除了增加以外,可能还需要一个删除功能,那就再提供一个定时器命名的功能,用于增删改查定时器是本身
  • 再加个测试函数。。完美

直接上代码:

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253 ----------------------------------------------任何一个记录产生一个实例local
Clock = {}
local
Clock_mt = {__index = Clock}
 local function __checkPositiveInteger(name, value)    if type(value) ~= "number" or value < 0 then        error(name .. " must be a positive number")    endend --验证是否可执行local function __isCallable(callback)    local tc = type(callback)    if tc == 'function' then return true end    if tc == 'table' then        local mt = getmetatable(callback)        return type(mt) == 'table' and type(mt.__call) == 'function'    end    return falseend local function newClock(cid, name, time, callback, update, args)    assert(time)    assert(callback)    assert(__isCallable(callback), "callback must be a function")    return setmetatable({        cid      = cid,        name     = name,        time     = time,        callback = callback,        args     = args,        running  = 0,        update   = update    }, Clock_mt)end function Clock:reset(running)    running = running or 0    __checkPositiveInteger('running', running)     self.running = running    self.deleted = nil          --如果已经删除的,也要复活end local function updateEveryClock(self, dt)    __checkPositiveInteger('dt', dt)    self.running = self.running + dt     while self.running >= self.time do        self.callback(unpack(self.args))        self.running = self.running - self.time    end    return falseend local function updateAfterClock(self, dt) -- returns true if expired    __checkPositiveInteger('dt', dt)    if self.running >= self.time then return true end     self.running = self.running + dt     if self.running >= self.time then        self.callback(unpack(self.args))        return true    end    return falseend local function match( left, right )    if left == '*' then return true end     --单整数的情况    if 'number' == type(left) and left == right then        return true    end     --范围的情况 形如 1-12/5,算了,先不支持这种每隔几分钟的这种特性吧    _,_,a,b = string.find(left, "(%d+)-(%d+)")    if a and b then        return (right >= tonumber(a) and right <= tonumber(b))    end     --多选项的情况 形如 1,2,3,4,5    --哎,luajit不支持gfind,    --for in string.gfind(left, "%d+"do    --其实也可以for in string.gmatch(left,'(%d+)'do    local pos = 0    for st,sp in function() return string.find(left, ',', pos, true) end do        if tonumber(string.sub(left, pos, st - 1)) == right then            return true        end        pos = sp + 1    end    return tonumber(string.sub(left, pos)) == rightend local function updateCrontab( self, dt )    local now = os.date('*t')    local tm = self.time    --print('updateCrontab/now:',   now.min, now.hour,  now.day,    now.month,  now.wday)    --print('updateCrontab/tm', tm.mn, tm.hr, tm.day, tm.mon, tm.wkd)    --print('match:',match(tm.mn, now.min), match(tm.hr, now.hour), match(tm.day, now.day), match(tm.mon, now.month), match(tm.wkd, now.wday))    if match(tm.mn, now.min) and match(tm.hr, now.hour)        and match(tm.day, now.day) and match(tm.mon, now.month)        and match(tm.wkd, now.wday)    then        --print('matching',self.name,self.callback,self.running)        self.callback(unpack(self.args))        self.running = self.running + 1    end    return falseend --遍历并执行所有的定时器local function updateClockTables( tbl )    for i = #tbl, 1, -1 do        local v = tbl[i]        if v.deleted == true or v:update(1) then            table.remove(tbl,i)        end    endend ---------------------------------------------------------- local
crontab = {}
crontab.__index
= crontab
 function crontab.new( obj )    local obj = obj or {}    setmetatable(obj, crontab)    --执行一下构造函数    if obj.ctor then        obj.ctor(obj)    end    return objend function crontab:ctor(  )    --所有的定时器    self._clocks = self._clocks or {}    self._crons = self._crons or {}    --累积的时间差    self._diff = self._diff or 0    --已命名的定时器,设置为弱引用表    self._nameObj = {}    setmetatable(self._nameObj, {__mode="k,v"})     --取得现在的秒数,延迟到整点分钟的时候启动一个定时    self:after("__delayUpdateCrontab", 60-os.time()%60, function ( )        --在整点分钟的时候,每隔一分钟执行一次        self:every("__updateCrontab", 60, function ( )                             updateClockTables(self._crons)        end)    end)end function crontab:update( diff )    self._diff = self._diff + diff    while self._diff >= 1000 do        --TODO:这里真让人纠结,要不要支持累积时间误差呢?        self._diff = self._diff - 1000        --开始对所有的定时器心跳,如果返回true,则从列表中移除        updateClockTables(self._clocks)    endend function crontab:remove( name )    if name and self._nameObj[name] then        self._nameObj[name].deleted = true    endend --通过判断callback的真正位置,以及参数类型来支持可变参数--返回值顺序
number, string, number, 
function, args
--总的有如下5种情况--1)
cid,name,time,callback,args
--2)
name,cid,time,callback,args
--3)
name,time,callback,args
--4)
cid,time,callback,args
--5)
time,callback,args
local function changeParamsName( p1, p2, p3, p4, p5 )    if __isCallable(p4) then        if type(p1) == 'string' then            return p2,p1,p3,p4,p5        else            return p1,p2,p3,p4,p5        end    elseif __isCallable(p3) then        if type(p1) == 'string' then            return nil,p1,p2,p3,p4        else            return p1,nil,p2,p3,p4        end    else        return nil,nil,p1,p2,p3    endend function crontab:every( cid, name, time, callback, args )    --支持可变参数    cid, name, time, callback, args = changeParamsName(cid, name, time, callback,args)    __checkPositiveInteger('time', time)    local clock = newClock(cid, name, time, callback, updateEveryClock, args or {})    table.insert(self._clocks,clock)    if name and name ~= '' then        self._nameObj[name] = clock    end    return clockend function crontab:after( cid, name, time, callback, args )    cid, name, time, callback, args = changeParamsName(cid, name, time, callback,args)    __checkPositiveInteger('time', time)    local clock = newClock(cid, name, time, callback, updateAfterClock, args or {})    table.insert(self._clocks,clock)    if name and name ~= '' then        self._nameObj[name] = clock    end    return clockend --增加计划任务,精度到达分钟级别--表达式:分钟[0-59]
小时[0-23] 每月的几号[1-31] 月份[1-12] 星期几[1-7]
--         
星期天为1,
--          "*"代表所有的取值范围内的数字--          "-"代表从某个数字到某个数字--          "/"代表每的意思,如"*/5"表示每5个单位,未实现--          ","分散的数字-- 
如:
"45 4-23/5 1,10,22 * *"
function crontab:addCron(cid, name, crontab_str, callback, args )    cid, name, crontab_str, callback, args = changeParamsName(cid, name, crontab_str, callback, args)    --print(cid, name, crontab_str, callback)    local t = {}    for in string.gmatch(crontab_str,'[%w._/,%-*]+'do        --如果可以转成整型直接转了,等下直接对比        local i = tonumber(v)        table.insert(t, i and i or v)    end    if table.getn(t) ~= 5 then        return error(string.format('crontab string,[%s] error!',crontab_str))    end     local time = {mn = t[1], hr = t[2], day = t[3], mon = t[4], wkd = t[5]}    local clock = newClock(cid, name, time, callback, updateCrontab, args or {})    table.insert(self._crons,clock)    if name and name ~= '' then        self._nameObj[name] = clock    endend return crontab

  

再看看测试代码:

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455 --传说中的测试代码local function RunTests()    -- the following calls are equivalent:    local function printMessage(a )      print('Hello',a)    end     local cron = crontab.new()     local c1 = cron:after( 5, printMessage)    local c2 = cron:after( 5, print, {'Hello'})     c1:update(2) -- will print nothing, the action is not done yet    c1:update(5) -- will print 'Hello' once     c1:reset() -- reset the counter to 0     -- prints 'hey' 5 times and then prints 'hello'    while not c1:update(1) do      print('hey')    end     -- Create a periodical clock:    local c3 = cron:every( 10, printMessage)     c3:update(5) -- nothing (total time: 5)    c3:update(4) -- nothing (total time: 9)    c3:update(12) -- prints 'Hello' twice (total time is now 21)     -------------------------------------    c1.deleted = true    c2.deleted = true    c3.deleted = true     ------------------------------    --测试一下match    print('----------------------------------')    assert(match('*',14) == true)    assert(match('12-15',14) == true)    assert(match('18-21',14) == false)    assert(match('18,21',14) == false)    assert(match('18,21,14',14) == true)     --加一个定时器1分钟后执行    cron:update(1000)     --加入一个定时器每分钟执行    cron:addCron('每秒执行''* * * * *', print, {'.......... cron'})     cron:update((60-os.time()%60)*1000)    cron:update(30*1000)    cron:update(31*1000)    cron:update(1)    cron:update(60*1000)        --打印两次end

  

也可以直接到 https://github.com/linbc/crontab.lua  下载代码

参考资料:

http://www.cise.ufl.edu/~cop4600/cgi-bin/lxr/http/source.cgi/commands/simple/cron.c

https://github.com/kikito/cron.lua