├── task.go ├── license.txt ├── queue_test.go ├── README.md └── queue.go /task.go: -------------------------------------------------------------------------------- 1 | package batchqueue 2 | 3 | type Runner struct { 4 | time uint64 5 | timeout uint64 6 | task Task 7 | next *Runner 8 | } 9 | 10 | type Task interface { 11 | BatchRun(queue *Queue, tasks []Task) 12 | } 13 | 14 | type TaskNode struct { 15 | task *Runner 16 | next *TaskNode 17 | } 18 | 19 | type TaskList struct { 20 | head *Runner 21 | } 22 | -------------------------------------------------------------------------------- /license.txt: -------------------------------------------------------------------------------- 1 | Copyright 2013 Hui Chen 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /queue_test.go: -------------------------------------------------------------------------------- 1 | package batchqueue 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | type MyTask struct { 10 | id string 11 | } 12 | 13 | func (t MyTask) BatchRun(queue *Queue, tasks []Task) { 14 | output := "" 15 | for _, t := range tasks { 16 | output += fmt.Sprintf("%s ", t.(MyTask).id) 17 | } 18 | fmt.Println(output) 19 | } 20 | 21 | func TestAddTask(t *testing.T) { 22 | var queue Queue 23 | options := InitOptions{ 24 | TimeUnit: 1000000, // 时间单位一毫秒 25 | NumWorkers: 1, 26 | NumTasksPerBatch: 2} 27 | queue.Init(options) 28 | queue.AddTask(10, 0, MyTask{"10"}) 29 | queue.AddTask(0, 5, MyTask{"0"}) 30 | queue.AddTask(7, 2, MyTask{"7"}) 31 | queue.AddTask(1, 5, MyTask{"1"}) 32 | queue.AddTask(9, 5, MyTask{"delete"}) 33 | queue.AddTask(8, 5, MyTask{"delete"}) 34 | queue.AddTask(3, 5, MyTask{"3"}) 35 | queue.AddTask(2, 5, MyTask{"2"}) 36 | queue.AddTask(17, 5, MyTask{"17"}) 37 | queue.RemoveTasks(MyTask{"delete"}) 38 | fmt.Println("任务总数", queue.NumTasks()) 39 | time.Sleep(time.Nanosecond * time.Duration(options.TimeUnit*20)) 40 | } 41 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## 这是什么 2 | 3 | 你有一个女秘书,她负责帮你从楼下传达室收包裹。当你需要取包裹的时候,只要写张小纸条放她桌上,她就会立刻去取,如果她还在去传达室的路上,你发现又有一个包裹需要取了,就再写一张小纸条放她桌子上。她回来之后发现桌子上一沓纸条,她会取出最下面那张(也就是最早那张)再去取,然后回来再拿一张纸条再取,直到纸条取完为止。她的力气比较小,而你的包裹都特别重,她只好一件一件去拿。这就是她一天的所有工作,你只需要给她纸条然后就什么都不需要管,下班的时候所有的包裹已经送到了你的办公室。她的名字叫 **任务队列** (task queue)。 4 | 5 | 这位秘书的勤勉让懒惰的你感到汗颜,于是你开除了她,然后雇了一个比较懒惰的。新的秘书稍微聪明些,经过多天对你的观察,发现你收到的包裹都是从淘宝买给家里用的东西,而且你每天准时下午三点带这些包裹回家,也就是说你三点之前不需要这些包裹。新秘书对此发现欣喜若狂,于是她每天下午2点半才到办公室上班,然后从两点半到三点清点她桌子上的纸条,一件一件地从传达室拿包裹(她的力气也比较小)。这位秘书叫 **延迟任务队列** (delayed task queue)。 6 | 7 | 时间就这样一天天过去了,直到有一天。。。你前几天从淘宝买的100件商品到了,这位秘书和她的小伙伴对你浪费公司人力资源的行为都惊呆了,而给她取包裹的时间只有半个小时!她以最快的速度把肝儿都累出来了还是没能在三点之前搬完,于是她两点半才上班的小秘密被你发现了。你无情地开除了她!这不公平! 8 | 9 | 于是,就有了你的现任,一位地道的女汉子。她也是每天两点半上班取包裹,但不同的是,她是位肌肉型秘书,一次可以搬十件包裹!这就极大缩短了搬运时间,因为她可以一次从桌子上抽十张纸条进行批量处理。从此以后,无论你从淘宝上买了多少东西,在三点之前他们都会一件不少地整齐地摆放在你的办公室里等着你带回家。你终于找到了心中梦寐以求的完美秘书,她的名字叫 **批处理延迟任务队列** (batch delayed task queue)。 10 | 11 | ## 安装/更新 12 | 13 | go get -u github.com/huichen/batchqueue 14 | 15 | ## 使用 16 | 17 | 定义你的任务,任务必须继承batchqueue.Task接口,也就是需要实现下面的BatchRun函数 18 | 19 | ```go 20 | type MyTask struct { 21 | } 22 | 23 | // 批处理运行任务,tasks切片中的第一个任务就是此任务自己 24 | func (t MyTask) BatchRun(queue *batchqueue.Queue, tasks []Task) { 25 | for _, task := range tasks { 26 | myTask := task.(MyTask) 27 | // 做点儿什么 28 | } 29 | } 30 | ``` 31 | 32 | 然后就可以用了 33 | ```go 34 | var queue batchqueue.Queue 35 | options := batchqueue.InitOptions{ 36 | TimeUnit: 1000000, // 时间单位一毫秒,也就是最短的时间间隔 37 | NumWorkers: 10, // 开辟多少个协程完成工作 38 | NumTasksPerBatch: 2} // 一次最多能搬运多少个箱子 39 | queue.Init(options) 40 | 41 | // 加入任务到队列, 42 | // 第一个参数是从现在开始的延迟时间,也就是说十个时间单位后开始做该任务 43 | // 第二个参数是任务最大延迟,也就是说要在从现在开始的第十三个时间单位之前做掉 44 | // 第三个参数是任务本身 45 | queue.AddTask(10, 3, MyTask{}) 46 | // 可以反复添加任务 47 | ``` 48 | 49 | 具体用法见[queue.go](/queue.go)中注释。 50 | 51 | **注意** 52 | 53 | * 当NumTasksPerBatch>1时,队列中的任务必须是同质的,也就是同样的类。否则BatchRun函数无法完成批量操作。 54 | 如果需要不同质的批处理任务,请给每种类型的任务开辟一个队列。 55 | * 当NumTasksPerBathc=1时,队列中的任务可以不同质,这实际上退化成了无批处理的延迟任务队列 56 | -------------------------------------------------------------------------------- /queue.go: -------------------------------------------------------------------------------- 1 | package batchqueue 2 | 3 | import ( 4 | "log" 5 | "runtime" 6 | "sync" 7 | "time" 8 | ) 9 | 10 | // 批处理延迟任务队列 11 | type Queue struct { 12 | sync.RWMutex 13 | taskList TaskList 14 | timeUnit uint64 15 | startTime time.Time 16 | numTasksPerBatch int 17 | runnerChannel chan []Task 18 | isInitialized bool 19 | numTasks uint64 20 | } 21 | 22 | type InitOptions struct { 23 | // 一个时间单位包含的纳秒数。这是任务执行的最小时间间隔。 24 | TimeUnit uint64 25 | 26 | // 执行批处理操作的最大协程数目,至少为1。 27 | // 请根据负载合理设置此值,否则AddTask可能会产生阻塞。 28 | NumWorkers int 29 | 30 | // 批处理最大的任务数目。 31 | NumTasksPerBatch int 32 | } 33 | 34 | // 初始化任务队列,并开始计时。 35 | func (q *Queue) Init(options InitOptions) { 36 | if q.isInitialized { 37 | log.Fatal("不能重复初始化batchqueue") 38 | } 39 | q.isInitialized = true 40 | 41 | q.timeUnit = options.TimeUnit 42 | q.startTime = time.Now() 43 | if options.NumTasksPerBatch <= 0 { 44 | q.numTasksPerBatch = 1 45 | } else { 46 | q.numTasksPerBatch = options.NumTasksPerBatch 47 | } 48 | 49 | runtime.GOMAXPROCS(runtime.NumCPU()) 50 | if options.NumWorkers <= 0 { 51 | log.Fatal("InitOptions.NumWorkers必须大于零") 52 | } 53 | q.runnerChannel = make(chan []Task, options.NumWorkers) 54 | for i := 0; i < options.NumWorkers; i++ { 55 | go q.worker() 56 | } 57 | go q.start() 58 | } 59 | 60 | // 在当前时间加delay个时间单位后执行任务,任务的过期时间(从delay之后开始算) 61 | // 为timeout个时间单位。也就是说,任务的执行时间窗口为 62 | // [now+delay, now+delay+timeout] 63 | func (q *Queue) AddTask(delay uint64, timeout uint64, task Task) { 64 | if !q.isInitialized { 65 | log.Fatal("必须先初始化batchqueue") 66 | } 67 | runner := new(Runner) 68 | runner.task = task 69 | runner.time = q.Now() + delay 70 | runner.timeout = timeout 71 | q.Lock() 72 | q.insert(&(q.taskList), runner) 73 | q.numTasks++ 74 | q.Unlock() 75 | } 76 | 77 | // 在队列中删除所有等于task的任务。请慎重设计你的Task类,保证其中带有 78 | // 可以识别的ID等信息能够区别不同的任务,否则所有任务都会被删除! 79 | func (q *Queue) RemoveTasks(task Task) { 80 | if !q.isInitialized { 81 | log.Fatal("必须先初始化batchqueue") 82 | } 83 | 84 | q.Lock() 85 | current := q.taskList.head 86 | for ; current != nil && current.task == task; current = current.next { 87 | q.taskList.head = current.next 88 | q.numTasks-- 89 | } 90 | for current.next != nil { 91 | if current.next.task == task { 92 | current.next = current.next.next 93 | q.numTasks-- 94 | } else { 95 | current = current.next 96 | } 97 | } 98 | q.Unlock() 99 | } 100 | 101 | // 当前时间,以Init调用开始为零点。单位为初始化时定义的时间单位。 102 | func (q *Queue) Now() uint64 { 103 | if !q.isInitialized { 104 | log.Fatal("必须先初始化batchqueue") 105 | } 106 | return uint64(time.Now().Sub(q.startTime).Nanoseconds()) / q.timeUnit 107 | } 108 | 109 | // 返回队列中的任务总数 110 | func (q *Queue) NumTasks() uint64 { 111 | return q.numTasks 112 | } 113 | 114 | func (q *Queue) insert(list *TaskList, runner *Runner) { 115 | if list.head == nil { 116 | list.head = runner 117 | return 118 | } 119 | 120 | if list.head.time > runner.time { 121 | runner.next = list.head 122 | list.head = runner 123 | return 124 | } 125 | 126 | current := list.head 127 | for ; current.next != nil && current.next.time <= runner.time; current = current.next { 128 | } 129 | 130 | if current.next == nil { 131 | current.next = runner 132 | } else { 133 | runner.next = current.next 134 | current.next = runner 135 | } 136 | } 137 | 138 | func (q *Queue) start() { 139 | oldTick := q.Now() 140 | for { 141 | q.Lock() 142 | if q.taskList.head == nil { 143 | continue 144 | } 145 | tasks := make([]Task, q.numTasksPerBatch) 146 | expiredTasks := make([]Task, q.numTasksPerBatch) 147 | aliveRunners := make([]*Runner, q.numTasksPerBatch) 148 | now := q.Now() 149 | taskCount := 0 150 | current := q.taskList.head 151 | numExpiredTasks := 0 152 | numAliveRunners := 0 153 | for ; current != nil && current.time <= now; current = current.next { 154 | if current.timeout+current.time <= now { 155 | expiredTasks[numExpiredTasks] = current.task 156 | numExpiredTasks++ 157 | } else { 158 | aliveRunners[numAliveRunners] = current 159 | numAliveRunners++ 160 | } 161 | tasks[taskCount] = current.task 162 | taskCount++ 163 | if taskCount >= q.numTasksPerBatch { 164 | q.runnerChannel <- tasks 165 | q.numTasks -= uint64(len(tasks)) 166 | tasks = make([]Task, q.numTasksPerBatch) 167 | taskCount = 0 168 | numExpiredTasks = 0 169 | numAliveRunners = 0 170 | } 171 | } 172 | 173 | if numExpiredTasks != 0 { 174 | q.numTasks -= uint64(numExpiredTasks) 175 | q.runnerChannel <- expiredTasks[0:numExpiredTasks] 176 | } 177 | 178 | if numAliveRunners != 0 { 179 | q.taskList.head = aliveRunners[0] 180 | for iRunner := 0; iRunner < numAliveRunners-1; iRunner++ { 181 | aliveRunners[iRunner].next = aliveRunners[iRunner+1] 182 | } 183 | aliveRunners[numAliveRunners-1].next = current 184 | } else { 185 | q.taskList.head = current 186 | } 187 | q.Unlock() 188 | 189 | // 等待下一时间 190 | newTick := q.Now() + 1 191 | if newTick == oldTick { 192 | newTick++ 193 | } 194 | delay := q.startTime.Add( 195 | time.Nanosecond * time.Duration(q.timeUnit*newTick)).Sub(time.Now()) 196 | select { 197 | case <-time.After(delay): 198 | } 199 | oldTick = newTick 200 | } 201 | } 202 | 203 | func (q *Queue) worker() { 204 | for { 205 | tasks := <-q.runnerChannel 206 | if len(tasks) > 0 { 207 | tasks[0].BatchRun(q, tasks) 208 | } 209 | } 210 | } 211 | --------------------------------------------------------------------------------