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

来源:互联网 发布:织梦小说源码 编辑:程序博客网 时间:2024/06/05 22:31



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

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

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

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

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

直接上代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
--------------------------------------------
--任何一个记录产生一个实例
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")
    end
end
 
--验证是否可执行
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 false
end
 
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 false
end
 
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 false
end
 
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)) == right
end
 
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 false
end
 
--遍历并执行所有的定时器
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
    end
end
 
----------------------------------------------------------
 
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 obj
end
 
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)
    end
end
 
function crontab:remove( name )
    if name and self._nameObj[name] then
        self._nameObj[name].deleted = true
    end
end
 
--通过判断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
    end
end
 
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 clock
end
 
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 clock
end
 
--增加计划任务,精度到达分钟级别
--表达式:分钟[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
    end
end
 
return crontab

  

再看看测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
--传说中的测试代码
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


0 0
原创粉丝点击