https://www.cnblogs.com/jiangz222/p/12345566.htmlhtml
robfig/cron是GO语言中一个定时执行注册任务的package, 最近我在工程中使用到了它,因为它的实现优雅且简单(主要是简单),因此将源码过了一遍,记录和分享在此。git
文档:http://godoc.org/github.com/robfig/cron,repo: https://github.com/robfig/crongithub
基本玩法golang
Demo代码以下,先用cron.New()初始化一个实例,而后调用AddFunc(spec string, cmd func()) 注册你但愿调用的func,第一个参数为调度的时间策略,第二个参数为到时间后执行的方法。robfig/cron支持很是多样的时间策略(下面的代码举了一些例子),最后经过cron.Start()方法启动。 数据结构
func TestCronDemo(t *testing.T) { c := cron.New() // 经过AddFunc注册 c.AddFunc("30 * * * *", func() { fmt.Println("Every hour on the half hour") }) c.AddFunc("30 3-6,20-23 * * *", func() { fmt.Println(".. in the range 3-6am, 8-11pm") }) c.AddFunc("CRON_TZ=Asia/Tokyo 30 04 * * *", func() { fmt.Println("Runs at 04:30 Tokyo time every day") }) c.AddFunc("@every 5m", func() { fmt.Println("every 5m, start 5m fron now") }) // 经过AddJob注册 // var cJob cronJobDemo // c.AddJob("@every 5s", cJob) // 启动 c.Start() // 中止 c.Stop() } type cronJobDemo int func (c cronJobDemo) Run() { fmt.Println("5s func trigger") return }
上面代码中,第九、10行的代码调用方法AddJob(spec string, cmd Job)也能够实现AddFunc注册的功能,Job是interface,须要入参类型实现方法:Run()。实际上,方法AddFunc内部将参数cmd 进行了包装(wrapper),而后也是调用方法AddJob进行注册。
若是实际工程中定时执行的逻辑较为复杂,推荐使用方法AddJob()来注册,本身写方法Run(),这样能够经过Run所属的类型来传递所需数据,后面介绍都会说成AddJob,等效于AddFunc。app
AddJob后发生了什么? (主要的数据结构)学习
对于Cron的总体逻辑,最关键的两个数据结构就是struct Entry和Cron。spa
每当你用AddJob注册一个定时调用策略,就会为这个策略生成一个惟一的Entry,不难想象,Entry里会存储被执行的时间、须要被调度执行的实体Job。code
生成entry后,再将entry放到struct Cron的entry列表里,Cron的结构里,主要是一些用来和外部交互的channel,好比经过channel添加、删除entry等。详见下面的代码。htm
// Entry 数据结构,每个被调度实体一个 type Entry struct { // 惟一id,用于查询和删除 ID EntryID // 本Entry的调度时间,不是绝对时间,在生成entry时会计算出来 Schedule Schedule // 本entry下次须要执行的绝对时间,会一直被更新 // 被封装的含义是Job能够多层嵌套,能够实现基于须要执行Job的额外处理 // 好比抓取Job异常、若是Job没有返回下一个时间点的Job是仍是继续执行仍是delay Next time.Time // 上一次被执行时间,主要用来查询 Prev time.Time // WrappedJob 是真实执行的Job实体 WrappedJob Job // Job 主要给用户查询 Job Job } // Cron 数据结构,为robfig/cron的运行实体使用的s数据结构 type Cron struct { entries []*Entry // 调度执行实体列表 // chain 用来定义entry里的warppedJob使用什么逻辑(e.g. skipIfLastRunning) // 即一个cron里全部entry只有一个封装逻辑 chain Chain stop chan struct{} // 中止整个cron的channel add chan *Entry // 增长一个entry的channel remove chan EntryID // 移除一个entry的channel snapshot chan chan []Entry // 获取entry总体快照的channel running bool // 表明是否已经在执行,是cron为使用者提供的动态修改entry的接口准备的 logger Logger // 封装golang的log包 runningMu sync.Mutex // 用来修改运行中的cron数据,好比增长entry,移除entry location *time.Location // 地理位置 parser ScheduleParser // 对时间格式的解析,为interface, 能够定制本身的时间规则。 nextID EntryID // entry的全局ID,新增一个entry就加1 jobWaiter sync.WaitGroup // run job时会进行add(1), job 结束会done(),stop整个cron,以此保证全部job都能退出 }
须要注意的是,WrappedJob和chain这两个成员,这是Cron实现的Job封装逻辑,目前是解决实际调度Job的异常处理。好比你但愿本身的上一个时间点的JobA没有结束,下一个时间点的JobA就不执行,这个“不执行”的逻辑实现就定义在chain,初始化时经过chain将JobA进行封装写入WrappedJob,那么每次JobA调用前会先执行封装逻辑,进行判断。
Start后发生了什么? (程序的主体)
cron.Start()执行后,cron的后台程序(方法run())就开始运行了。而它的主体,就是一个定时器的实现和到时后的job运行,加上cron里的数据维护。
cron的定时器实现是一个简洁而典型的业务层实现,着重了解下,具体的流程图可见下图。
它的关键和值得学习之处是:
上面的逻辑说完,程序主体已经清晰,除此以外,程序主体里的定时器监听和其余多个channel共用了select-case,这些channel在struct Cron里能看到,实现了entries的动态添加、删除、entries快照获取等功能。代码结构以下:
将这些操做经过channel让程序主体来操做,能够有效的减小互斥锁的使用,也会引入问题,会致使有的job执行时间不是很是精准,致使某些entry被遗漏:
固然,channel的操做首先是很是简洁省时的,其次,定时器实现里,会扫描全部当前时间以前的entries来执行,增长了容错性
值得称赞的细节
interface的使用
struct Entry里的Schedule和Cron里的ScheduleParser都是interface,意味着咱们是能够本身定制注册job时的时间策略的格式的,只要本身实现时间策略的解析和获取方法就好
这让我想起了之前看过golang里何时用interface和struct的讨论,我以为这是个很好的例子:预期对同一个接口有多个实现时就抽象成interface,不知道该不应用就用struct。
wrapper的实现
上面有提到,经过对Job的封装,cron实现了同一个job屡次调用时的异常处理等,值得之后在实践中借鉴。
最后是我加了一点注释的代码,https://github.com/jiangz222/cron/tree/comments-v3