下面文章转自:
感谢原作者。
老大让我写个cron的笔记,好像好久没有写过东西了。。。
设置好gopath,使用命令go get github.com/robfig/cron
获取包支持
源码地址 https://github.com/robfig/cron
使用示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
package main
import ( "github.com/robfig/cron" "log" )
func main() { i := 0 c := cron.New() spec := "*/5 * * * * ?" c.AddFunc(spec, func() { i++ log.Println("cron running:", i) }) c.Start()
select{} }
|
字段名 | 是否必须 | 允许的值 | 允许的特定字符 |
---|---|---|---|
秒(Seconds) | 是 | 0-59 | * / , – |
分(Minutes) | 是 | 0-59 | * / , – |
时(Hours) | 是 | 0-23 | * / , – |
日(Day of month) | 是 | 1-31 | * / , – ? |
月(Month) | 是 | 1-12 or JAN-DEC | * / , – |
星期(Day of week) | 否 | 0-6 or SUM-SAT | * / , – ? |
1)星号(*)
表示 cron 表达式能匹配该字段的所有值。如在第5个字段使用星号(month),表示每个月
2)斜线(/)
表示增长间隔,如第1个字段(minutes) 值是 3-59/15,表示每小时的第3分钟开始执行一次,之后每隔 15 分钟执行一次(即 3、18、33、48 这些时间点执行),这里也可以表示为:3/15
3)逗号(,)
用于枚举值,如第6个字段值是 MON,WED,FRI,表示 星期一、三、五 执行
4)连字号(-)
表示一个范围,如第3个字段的值为 9-17 表示 9am 到 5pm 直接每个小时(包括9和17)
5)问号(?)
只用于 日(Day of month) 和 星期(Day of week),表示不指定值,可以用于代替 *
1.cron:包含一系列要执行的实体;支持暂停【stop】;添加实体等
1 2 3 4 5 6 7 8 |
type Cron struct { entries []*Entry stop chan struct{} // 控制 Cron 实例暂停 add chan *Entry // 当 Cron 已经运行了,增加新的 Entity 是通过 add 这个 channel 实现的 snapshot chan []*Entry // 获取当前所有 entity 的快照 running bool // 当已经运行时为true;否则为false }
|
注意,Cron 结构没有导出任何成员。
注意:有一个成员 stop,类型是 struct{},即空结构体。
2.Entry:调度实体
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
type Entry struct { // The schedule on which this job should be run. // 负责调度当前 Entity 中的 Job 执行 Schedule Schedule
// The next time the job will run. This is the zero time if Cron has not been // started or this entry's schedule is unsatisfiable // Job 下一次执行的时间 Next time.Time
// The last time this job was run. This is the zero time if the job has never // been run. // 上一次执行时间 Prev time.Time
// The Job to run. // 要执行的 Job Job Job }
|
3.Job:每一个实体包含一个需要运行的Job
这是一个接口,只有一个方法:run
1 2 3 4 |
type Job interface { Run() }
|
由于 Entity 中需要 Job 类型,因此,我们希望定期运行的任务,就需要实现 Job 接口。同时,由于
Job接口只有一个无参数无返回值的方法,为了使用方便,作者提供了一个类型:
1 2 3 |
type FuncJob func() func (f FuncJob) Run() { f() }
|
它通过简单的实现 Run() 方法来实现 Job 接口,这样,任何无参数无返回值的函数,通过强制类型转换为 FuncJob,就可以当作 Job 来使用了,AddFunc 方法 就是这么做的。所以需要修改带参数功能的job时从此处下手
Schedule:每个实体包含一个调度器(Schedule)
负责调度 Job 的执行。它也是一个接口。
1 2 3 4 5 6 7 |
type Schedule interface { // Return the next activation time, later than the given time. // Next is invoked initially, and then each time the job is run. // 返回同一 Entity 中的 Job 下一次执行的时间 Next(time.Time) time.Time }
|
Schedule 的具体实现通过解析 Cron 表达式得到。
库中提供了 Schedule 的两个具体实现,分别是 SpecSchedule 和 ConstantDelaySchedule。
1 2 3 4 |
type SpecSchedule struct { Second, Minute, Hour, Dom, Month, Dow uint64 }
|
1 2 3 4 |
type ConstantDelaySchedule struct { Delay time.Duration // 循环的时间间隔 }
|
这是一个简单的循环调度器,如:每 5 分钟。注意,最小单位是秒,不能比秒还小,比如 毫秒。
通过 Every 函数可以获取该类型的实例,如:
1 2 |
constDelaySchedule := Every(5e9)
|
得到的是一个每 5 秒执行一次的调度器。
1. 函数
1 2 3 4 5 6 7 8 9 10 |
func New() *Cron { return &Cron{ entries: nil, add: make(chan *Entry), stop: make(chan struct{}), snapshot: make(chan []*Entry), running: false, } }
|
可见实例化时,成员使用的基本是默认值;
1 2 |
func Parse(spec string) (_ Schedule, err error)
|
spec 可以是:
* Full crontab specs, e.g. “* * * * * ?”
* Descriptors, e.g. “@midnight”, “@every 1h30m”
2.成员方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
// 将 job 加入 Cron 中 // 如上所述,该方法只是简单的通过 FuncJob 类型强制转换 cmd,然后调用 AddJob 方法 func (c *Cron) AddFunc(spec string, cmd func()) error
// 将 job 加入 Cron 中 // 通过 Parse 函数解析 cron 表达式 spec 的到调度器实例(Schedule),之后调用 c.Schedule 方法 func (c *Cron) AddJob(spec string, cmd Job) error
// 获取当前 Cron 总所有 Entities 的快照 func (c *Cron) Entries() []*Entry
// 通过两个参数实例化一个 Entity,然后加入当前 Cron 中 // 注意:如果当前 Cron 未运行,则直接将该 entity 加入 Cron 中; // 否则,通过 add 这个成员 channel 将 entity 加入正在运行的 Cron 中 func (c *Cron) Schedule(schedule Schedule, cmd Job)
// 新启动一个 goroutine 运行当前 Cron func (c *Cron) Start()
// 通过给 stop 成员发送一个 struct{}{} 来停止当前 Cron,同时将 running 置为 false // 从这里知道,stop 只是通知 Cron 停止,因此往 channel 发一个值即可,而不关心值是多少 // 所以,成员 stop 定义为空 struct func (c *Cron) Stop()
|
这几个函数就是文章开头的example中使用的函数,
1 2 3 4 |
func (c *Cron) AddFunc(spec string, cmd func()) error { return c.AddJob(spec, FuncJob(cmd)) }
|
AddFunc 含有两个参数,第一个是 cron表达式,这个不解释,第二个是func()类型参数cmd 即无参数无返回类型函数,下一步中直接将此参数强制转换为FuncJob类型,并调用AddJob函数
1 2 3 4 5 |
// A wrapper that turns a func() into a cron.Job type FuncJob func()
func (f FuncJob) Run() { f() }
|
由上述代码可知FuncJob为自定义类型,真实类型为 func(),此类型实现了一个Run()方法
1 2 3 4 5 6 7 8 9 10 |
// AddJob adds a Job to the Cron to be run on the given schedule. func (c *Cron) AddJob(spec string, cmd Job) error { schedule, err := Parse(spec) if err != nil { return err } c.Schedule(schedule, cmd) return nil }
|
首先 AddJob 函数的传入参数为一个string类型的cron表达式和一个Job类型的cmd参数,但在AddFunc函数中,我们传入的第二个参数为FuncJob类型,所以Job类型应该是一个接口,在解析了cron表达式无错误以后,调用Schedule方法将cmd添加进了调度器
1 2 3 4 5 |
// Job is an interface for submitted cron jobs. type Job interface { Run() }
|
由此可知,Job是带有一个Run方法的接口类型,经过代码分析可以指定,cron定时调度时间到达时,将调用此
方法,也就是意味着,任何实现了Run方法的实例,都可以作为AddJob函数的cmd参数,而Run方法所实现的内容
就是你定时调度所需执行的任务(AddFunc函数只能添加无参数无返回的任务,太鸡肋了),接下来我们就来实现一
个带参数的任务添加
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 |
//定义一个类型 包含一个int类型参数和函数体 type funcIntJob struct { num int function func(int) }
//实现这个类型的Run()方法 使得可以传入Job接口 func (this *funcIntJob) Run() { if nil != this.function { this.function(this.num) } }
//非必须 返回一个urlServeJob指针 func newfuncIntJob(num int, function funcInt) *urlServeJob { instance := &funcIntJob{ num: num, function: function, } return instance }
//示例任务 func shownum(num int){ fmt.Println(num) }
func main(){ var c = cron.New() job := newfuncIntJob(3, shownum) spec := "*/5 * * * * ?" c.AddJob(spec, job) c.Start() defer c.Stop() select{} }
|
我们就以AddFunc函数的之后开始分析cron的调度
前面说到,AddFunc函数调用了AddJob函数,这里再看一次AddJob函数
1 2 3 4 5 6 7 8 9 10 |
// AddJob adds a Job to the Cron to be run on the given schedule. func (c *Cron) AddJob(spec string, cmd Job) error { schedule, err := Parse(spec) if err != nil { return err } c.Schedule(schedule, cmd) return nil }
|
这里有两个值得注意的东东,cron表达式解析后的schedule变量,以及Cron的Schedule方法
前面搬砖的时候有说到Schedule类型也是一个接口,再看一眼
1 2 3 4 5 6 |
type Schedule interface { // Return the next activation time, later than the given time. // Next is invoked initially, and then each time the job is run. Next(time.Time) time.Time }
|
接口只有一个方法Next,参数返回都是Time,看样子应该是传入时间返回下次执行时间的一个方法,不过,
有时候乱猜真的会害死人
总之,Parse函数在经过一系列复杂的解析之后,返回了一个SpecSchedule类型变量,就是长这样的
1 2 3 4 5 6 |
// SpecSchedule specifies a duty cycle (to the second granularity), based on a // traditional crontab specification. It is computed initially and stored as bit sets. type SpecSchedule struct { Second, Minute, Hour, Dom, Month, Dow uint64 }
|
这种类型含有一个Next方法,代码太复杂就不贴了,方法里面的东东我也不太懂,应该是,算了,不应该了(应该
输入参数加上是SpecSchedule的间隔后返回吧)
接下来看Cron类型的Schedule方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// Schedule adds a Job to the Cron to be run on the given schedule. func (c *Cron) Schedule(schedule Schedule, cmd Job) { entry := &Entry{ Schedule: schedule, Job: cmd, } if !c.running { c.entries = append(c.entries, entry) return }
c.add <- entry }
|
这个比较好理解,根据schedule和cmd参数构建了一个Entry变量,并且将这个变量添加进Cron的entries中
只不过在没有运行的时候直接添加,运行的时候通过chan添加,到这里就添加部分就告一段落了
调度的开始实施是从Cron.Start()函数开始的
1 2 3 4 5 6 7 8 9 |
// Start the cron scheduler in its own go-routine, or no-op if already started. func (c *Cron) Start() { if c.running { return } c.running = true go c.run() }
|
东西很少,就是开了一个routine执行任务,这里cron还提供了一个使用当前routine执行的方法Run(),
1 2 3 4 5 6 7 8 9 |
// Run the cron scheduler, or no-op if already running. func (c *Cron) Run() { if c.running { return } c.running = true c.run() }
|
先不管这些,接下来重点到run()方法
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 |
// Run the scheduler. this is private just due to the need to synchronize // access to the 'running' state variable. func (c *Cron) run() { // Figure out the next activation times for each entry. now := c.now() //以当前时区的方式获取当前时间 for _, entry := range c.entries { entry.Next = entry.Schedule.Next(now) //得到entries中的每一个entry更新下一次执行时间 }
for { // Determine the next entry to run. sort.Sort(byTime(c.entries)) //排序 得到最先要执行的entry
var timer *time.Timer if len(c.entries) == 0 || c.entries[0].Next.IsZero() { // If there are no entries yet, just sleep - it still handles new entries // and stop requests. timer = time.NewTimer(100000 * time.Hour) } else { timer = time.NewTimer(c.entries[0].Next.Sub(now)) //时间最近要执行Entry到现在的时间差 下面唤醒select }
for { //这个for有什么用??? select { case now = <-timer.C: //时间到 执行任务 now = now.In(c.location) //更新时间 // Run every entry whose next time was less than now for _, e := range c.entries { if e.Next.After(now) || e.Next.IsZero() { break } go c.runWithRecovery(e.Job) //执行任务 e.Prev = e.Next e.Next = e.Schedule.Next(now) //下一个要执行的时间 }
case newEntry := <-c.add: //运行中添加Entry timer.Stop() now = c.now() newEntry.Next = newEntry.Schedule.Next(now) c.entries = append(c.entries, newEntry)
case <-c.snapshot: // 快照 c.snapshot <- c.entrySnapshot() continue
case <-c.stop: //停止信号 timer.Stop() return }
break } } }
|
这里就基本是调度的所有内容了,有些东西写在上面注释就不解释了,我能解释的,也就只有这么多了
下面对这个调度相关的东西做下笔记
这个函数主要调用了Schedule的Next方法,Schedule是一个接口,在前面我们知道,实际上在解析spec的时
候返回的变量是SpecSchedule类型,所以此处应该调用SpecSchedule的Next方法,这个方法就是上面说的
那个复杂不贴代码的方法,在网上找了个带注释的版本,反正就是得到这个entry下次执行的时间吧
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 |
func (s *SpecSchedule) Next(t time.Time) time.Time { // 秒级别的取整 t = t.Add(1*time.Second - time.Duration(t.Nanosecond())*time.Nanosecond)
// 判断一个字段是否被累加,如果是, 那么它的下一级别的字段需要归 0 。 added := false
//到未来的探寻不超过5年 yearLimit := t.Year() + 5 // 下一级别的字段累加到重置,需要重新累加上一级别的字段的时候的goto点 // 比如要找每个月的31号的时候, 4月是符合月份字段的规定的,但是4月的没有31号。 遍历尽4月的每一天后,只能请求重新累加月份。 WRAP: if t.Year() > yearLimit { return time.Time{} } // 月 for 1< // If we have to add a month, reset the other parts to 0. if !added { added = true // Otherwise, set the date at the beginning (since the current time is irrelevant). t = time.Date(t.Year(), t.Month(), 1, 0, 0, 0, 0, t.Location()) } t = t.AddDate(0, 1, 0) // Wrapped around. if t.Month() == time.January { goto WRAP } } // 天 , 一次处理 天/月 和 天/周 for !dayMatches(s, t) { if !added { added = true t = time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, t.Location()) } t = t.AddDate(0, 0, 1) if t.Day() == 1 { goto WRAP } } // 时 for 1< if !added { added = true t = t.Truncate(time.Hour) } t = t.Add(1 * time.Hour) if t.Hour() == 0 { goto WRAP } } // 分 for 1< if !added { added = true t = t.Truncate(time.Minute) } t = t.Add(1 * time.Minute) if t.Minute() == 0 { goto WRAP } } // 秒 for 1< if !added { added = true t = t.Truncate(time.Second) } t = t.Add(1 * time.Second) if t.Second() == 0 { goto WRAP } } return t } //一次处理 天/月 和 天/周 。 如果两者中有任意, 那么必须同时符合另一个才算是匹配 func dayMatches(s *SpecSchedule, t time.Time) bool { var ( domMatch bool = 1< dowMatch bool = 1< ) if s.Dom&starBit > 0 || s.Dow&starBit > 0 { return domMatch && dowMatch } return domMatch || dowMatch } |
初学者的摸爬滚打,对标准库还是太不了解了,如有错误,敬请指正
参考链接:
http://blog.studygolang.com/2014/02/go_crontab/
http://blog.csdn.net/cchd0001/article/details/51076922