├── .gitignore ├── go.mod ├── examples ├── use.go ├── panic_handler.go └── simple.go ├── tests └── panic_case.go ├── LICENSE.md ├── pool_test.go ├── pool.go └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .vscode -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/wazsmwazsm/mortar 2 | 3 | go 1.13 4 | -------------------------------------------------------------------------------- /examples/use.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/wazsmwazsm/mortar" 8 | ) 9 | 10 | func main() { 11 | pool, err := mortar.NewPool(10) 12 | if err != nil { 13 | panic(err) 14 | } 15 | 16 | for i := 0; i < 20; i++ { 17 | pool.Put(&mortar.Task{ 18 | Handler: func(v ...interface{}) { 19 | fmt.Println(v) 20 | }, 21 | Params: []interface{}{i}, 22 | }) 23 | } 24 | 25 | time.Sleep(1e9) 26 | } 27 | -------------------------------------------------------------------------------- /examples/panic_handler.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/wazsmwazsm/mortar" 8 | ) 9 | 10 | func main() { 11 | pool, err := mortar.NewPool(10) 12 | if err != nil { 13 | panic(err) 14 | } 15 | 16 | pool.PanicHandler = func(r interface{}) { 17 | fmt.Printf("Warning!!! %s", r) 18 | } 19 | 20 | pool.Put(&mortar.Task{ 21 | Handler: func(v ...interface{}) { 22 | panic("somthing wrong!") 23 | }, 24 | }) 25 | 26 | time.Sleep(1e9) 27 | } 28 | -------------------------------------------------------------------------------- /tests/panic_case.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/wazsmwazsm/mortar" 7 | ) 8 | 9 | func main() { 10 | case1() 11 | } 12 | 13 | func case1() { 14 | fmt.Println("--- case1 start ---") 15 | defer fmt.Println("--- case1 stoped ---") 16 | 17 | pool, err := mortar.NewPool(1) 18 | if err != nil { 19 | panic(err) 20 | } 21 | 22 | pool.Put(&mortar.Task{ 23 | Handler: func(v ...interface{}) { 24 | panic("aaaaaa!") 25 | }, 26 | Params: []interface{}{"hi!"}, 27 | }) 28 | 29 | pool.Put(&mortar.Task{ 30 | Handler: func(v ...interface{}) { 31 | fmt.Println(v) 32 | }, 33 | Params: []interface{}{"hi!"}, 34 | }) 35 | 36 | pool.Close() 37 | err = pool.Put(&mortar.Task{ 38 | Handler: func(v ...interface{}) {}, 39 | }) 40 | if err != nil { 41 | fmt.Println(err) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) MrQin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /examples/simple.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "sync" 6 | 7 | "github.com/wazsmwazsm/mortar" 8 | ) 9 | 10 | func main() { 11 | // 创建容量为 10 的任务池 12 | pool, err := mortar.NewPool(10) 13 | if err != nil { 14 | panic(err) 15 | } 16 | 17 | wg := new(sync.WaitGroup) 18 | 19 | for i := 0; i < 1000; i++ { 20 | wg.Add(1) 21 | // 创建任务 22 | task := &mortar.Task{ 23 | Handler: func(v ...interface{}) { 24 | wg.Done() 25 | fmt.Println(v) 26 | }, 27 | } 28 | // 添加任务函数的参数 29 | task.Params = []interface{}{i, i * 2, "hello"} 30 | // 将任务放入任务池 31 | pool.Put(task) 32 | } 33 | 34 | wg.Add(1) 35 | // 再创建一个任务 36 | pool.Put(&mortar.Task{ 37 | Handler: func(v ...interface{}) { 38 | wg.Done() 39 | fmt.Println(v) 40 | }, 41 | Params: []interface{}{"hi!"}, // 也可以在创建任务时设置参数 42 | }) 43 | 44 | wg.Wait() 45 | 46 | // 安全关闭任务池(保证已加入池中的任务被消费完) 47 | pool.Close() 48 | // 如果任务池已经关闭, Put() 方法会返回 ErrPoolAlreadyClosed 错误 49 | err = pool.Put(&mortar.Task{ 50 | Handler: func(v ...interface{}) {}, 51 | }) 52 | if err != nil { 53 | fmt.Println(err) // print: pool already closed 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /pool_test.go: -------------------------------------------------------------------------------- 1 | package mortar 2 | 3 | import ( 4 | "sync" 5 | "sync/atomic" 6 | "testing" 7 | ) 8 | 9 | var sum int64 10 | var runTimes = 1000000 11 | 12 | var wg = sync.WaitGroup{} 13 | 14 | func demoTask(v ...interface{}) { 15 | for i := 0; i < 100; i++ { 16 | atomic.AddInt64(&sum, 1) 17 | } 18 | } 19 | 20 | func demoTask2(v ...interface{}) { 21 | defer wg.Done() 22 | for i := 0; i < 100; i++ { 23 | atomic.AddInt64(&sum, 1) 24 | } 25 | } 26 | 27 | func BenchmarkGoroutine(b *testing.B) { 28 | for i := 0; i < b.N; i++ { 29 | go demoTask() 30 | } 31 | } 32 | 33 | func BenchmarkPut(b *testing.B) { 34 | pool, err := NewPool(10) 35 | if err != nil { 36 | b.Error(err) 37 | } 38 | 39 | task := &Task{ 40 | Handler: demoTask, 41 | } 42 | 43 | for i := 0; i < b.N; i++ { 44 | pool.Put(task) 45 | } 46 | } 47 | 48 | func BenchmarkGoroutineTimelife(b *testing.B) { 49 | for i := 0; i < b.N; i++ { 50 | wg.Add(1) 51 | go demoTask2() 52 | } 53 | wg.Wait() 54 | } 55 | 56 | func BenchmarkPutTimelife(b *testing.B) { 57 | pool, err := NewPool(10) 58 | if err != nil { 59 | b.Error(err) 60 | } 61 | 62 | task := &Task{ 63 | Handler: demoTask2, 64 | } 65 | 66 | for i := 0; i < b.N; i++ { 67 | wg.Add(1) 68 | pool.Put(task) 69 | } 70 | wg.Wait() 71 | 72 | } 73 | 74 | func BenchmarkGoroutineSetTimes(b *testing.B) { 75 | 76 | for i := 0; i < runTimes; i++ { 77 | go demoTask() 78 | } 79 | } 80 | 81 | func BenchmarkPoolPutSetTimes(b *testing.B) { 82 | pool, err := NewPool(20) 83 | if err != nil { 84 | b.Error(err) 85 | } 86 | 87 | task := &Task{ 88 | Handler: demoTask, 89 | } 90 | 91 | for i := 0; i < runTimes; i++ { 92 | pool.Put(task) 93 | } 94 | } 95 | 96 | func BenchmarkGoroutineTimeLifeSetTimes(b *testing.B) { 97 | 98 | for i := 0; i < runTimes; i++ { 99 | wg.Add(1) 100 | go demoTask2() 101 | } 102 | wg.Wait() 103 | } 104 | 105 | func BenchmarkPoolTimeLifeSetTimes(b *testing.B) { 106 | pool, err := NewPool(20) 107 | if err != nil { 108 | b.Error(err) 109 | } 110 | 111 | task := &Task{ 112 | Handler: demoTask2, 113 | } 114 | 115 | for i := 0; i < runTimes; i++ { 116 | wg.Add(1) 117 | pool.Put(task) 118 | } 119 | 120 | wg.Wait() 121 | } 122 | -------------------------------------------------------------------------------- /pool.go: -------------------------------------------------------------------------------- 1 | package mortar 2 | 3 | import ( 4 | "errors" 5 | "log" 6 | "sync" 7 | "sync/atomic" 8 | "time" 9 | ) 10 | 11 | // errors 12 | var ( 13 | // return if pool size <= 0 14 | ErrInvalidPoolCap = errors.New("invalid pool cap") 15 | // put task but pool already closed 16 | ErrPoolAlreadyClosed = errors.New("pool already closed") 17 | ) 18 | 19 | // running status 20 | const ( 21 | RUNNING = 1 22 | STOPED = 0 23 | ) 24 | 25 | // Task task to-do 26 | type Task struct { 27 | Handler func(v ...interface{}) 28 | Params []interface{} 29 | } 30 | 31 | // Pool task pool 32 | type Pool struct { 33 | capacity uint64 34 | runningWorkers uint64 35 | status int64 36 | chTask chan *Task 37 | PanicHandler func(interface{}) 38 | sync.Mutex 39 | } 40 | 41 | // NewPool init pool 42 | func NewPool(capacity uint64) (*Pool, error) { 43 | if capacity <= 0 { 44 | return nil, ErrInvalidPoolCap 45 | } 46 | p := &Pool{ 47 | capacity: capacity, 48 | status: RUNNING, 49 | chTask: make(chan *Task, capacity), 50 | } 51 | 52 | return p, nil 53 | } 54 | 55 | func (p *Pool) checkWorker() { 56 | p.Lock() 57 | defer p.Unlock() 58 | 59 | if p.runningWorkers == 0 && len(p.chTask) > 0 { 60 | p.run() 61 | } 62 | } 63 | 64 | // GetCap get capacity 65 | func (p *Pool) GetCap() uint64 { 66 | return p.capacity 67 | } 68 | 69 | // GetRunningWorkers get running workers 70 | func (p *Pool) GetRunningWorkers() uint64 { 71 | return atomic.LoadUint64(&p.runningWorkers) 72 | } 73 | 74 | func (p *Pool) incRunning() { 75 | atomic.AddUint64(&p.runningWorkers, 1) 76 | } 77 | 78 | func (p *Pool) decRunning() { 79 | atomic.AddUint64(&p.runningWorkers, ^uint64(0)) 80 | } 81 | 82 | // Put put a task to pool 83 | func (p *Pool) Put(task *Task) error { 84 | p.Lock() 85 | defer p.Unlock() 86 | 87 | if p.status == STOPED { 88 | return ErrPoolAlreadyClosed 89 | } 90 | 91 | // run worker 92 | if p.GetRunningWorkers() < p.GetCap() { 93 | p.run() 94 | } 95 | 96 | // send task 97 | if p.status == RUNNING { 98 | p.chTask <- task 99 | } 100 | 101 | return nil 102 | } 103 | 104 | func (p *Pool) run() { 105 | p.incRunning() 106 | 107 | go func() { 108 | defer func() { 109 | p.decRunning() 110 | if r := recover(); r != nil { 111 | if p.PanicHandler != nil { 112 | p.PanicHandler(r) 113 | } else { 114 | log.Printf("Worker panic: %s\n", r) 115 | } 116 | } 117 | p.checkWorker() // check worker avoid no worker running 118 | }() 119 | 120 | for { 121 | select { 122 | case task, ok := <-p.chTask: 123 | if !ok { 124 | return 125 | } 126 | task.Handler(task.Params...) 127 | } 128 | } 129 | }() 130 | } 131 | 132 | func (p *Pool) setStatus(status int64) bool { 133 | p.Lock() 134 | defer p.Unlock() 135 | 136 | if p.status == status { 137 | return false 138 | } 139 | 140 | p.status = status 141 | 142 | return true 143 | } 144 | 145 | // Close close pool graceful 146 | func (p *Pool) Close() { 147 | 148 | if !p.setStatus(STOPED) { // stop put task 149 | return 150 | } 151 | 152 | for len(p.chTask) > 0 { // wait all task be consumed 153 | time.Sleep(1e6) // reduce CPU load 154 | } 155 | 156 | close(p.chTask) 157 | } 158 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Mortar 2 | A goroutine task pool 3 | 4 | 一个简单好用的高性能任务池, 代码只有 100 多行。 5 | 6 | ## 版本更新日志 7 | 8 | ### v1.x 9 | #### v1.1 10 | - 部分冗余逻辑优化 11 | #### v1.2 12 | - 修复数据竞争 bug 13 | #### v1.3 14 | - 安全运行 worker 15 | #### v1.4 16 | - 退出等待 taskC 清空时增加 sleep 减少 cpu 负载 17 | #### v1.5 18 | - 优化锁,解决只有一个 woker 时产生 panic 后无发消费 task 导致 deadlock 的问题 (见 [issue 极端情况 #4](https://github.com/wazsmwazsm/mortar/issues/4)) 19 | 20 | ## 解决什么问题 21 | 22 | go 的 goroutine 提供了一种较线程而言更廉价的方式处理并发场景, 但 goroutine 太多会导致调度性能下降、GC 频繁、内存暴涨, 引发一系列问题。 23 | 24 | mortar 限制了最多可启动的 goroutine 数量, 同时保持和原生 goroutine 一样的性能(在海量 goroutine 场景优于原生 goroutine), 避免了上述问题。 25 | 26 | ## 原理 27 | 28 | 创建一个容量为 N 的池, 在池容量未满时, 每塞入一个任务(生产任务), 任务池开启一个 worker (建立协程) 去处理任务(消费任务)。 29 | 当任务池容量赛满,每塞入一个任务(生产任务), 任务会被已有的 N 个 worker 抢占执行(消费任务),达到协程限制的功能。 30 | 31 | ### 生产消费模型 32 | 33 | 队列: channel 34 | 35 | 生产任务: 将任务写入 channel 36 | 37 | 消费任务: worker(goroutine)从 channel 中读出任务执行 38 | 39 | ## 使用 40 | 41 | ### task struct 42 | 43 | 每个任务是一个结构体, Handler 是要执行的任务函数, Params 是要传入 Handler 的参数 44 | 45 | ```go 46 | type Task struct { 47 | Handler func(v ...interface{}) 48 | Params []interface{} 49 | } 50 | ``` 51 | 52 | ### NewPool 53 | 54 | NewPool() 方法创建一个任务池结构, 返回其指针 55 | 56 | ```go 57 | func NewPool(capacity uint64) (*Pool, error) 58 | ``` 59 | 60 | ### Put 61 | 62 | Put() 方法来将一个任务放入池中, 如果任务池未满, 则启动一个 worker。 63 | 64 | ```go 65 | func (p *Pool) Put(task *Task) error 66 | ``` 67 | 68 | ### GetCap 69 | 70 | 获取任务池容量, 创建任务池时已确定 71 | ```go 72 | func (p *Pool) GetCap() uint64 73 | ``` 74 | 75 | ### GetRunningWorkers 76 | 77 | 获取当前运行 worker 的数量 78 | ```go 79 | func (p *Pool) GetRunningWorkers() uint64 80 | ``` 81 | 82 | ### Close() 83 | 84 | 安全关闭任务池。Close() 方法会先阻止 Put() 方法继续放入任务, 等待所有任务都被消费运行后, 销毁所有 worker, 关闭任务 channel。 85 | ```go 86 | func (p *Pool) Close() 87 | ``` 88 | 89 | ### panic handler 90 | 91 | 每个 worker 都是一个原生 goroutine, 为保证程序的安全运行, 任务池会 recover 所有 worker 中的 panic, 并提供自定义的 panic 处理能力(不设置 PanicHandler 默认会打印 panic 的异常栈)。 92 | 93 | ```go 94 | pool.PanicHandler = func(r interface{}) { 95 | // handle panic 96 | log.Println(r) 97 | } 98 | ``` 99 | 100 | ## 安装 101 | ```bash 102 | go get github.com/wazsmwazsm/mortar 103 | ``` 104 | 105 | ## 例子 106 | 107 | ```go 108 | package main 109 | 110 | import ( 111 | "fmt" 112 | "github.com/wazsmwazsm/mortar" 113 | "sync" 114 | ) 115 | 116 | func main() { 117 | // 创建容量为 10 的任务池 118 | pool, err := mortar.NewPool(10) 119 | if err != nil { 120 | panic(err) 121 | } 122 | 123 | wg := new(sync.WaitGroup) 124 | 125 | for i := 0; i < 1000; i++ { 126 | wg.Add(1) 127 | // 创建任务 128 | task := &mortar.Task{ 129 | Handler: func(v ...interface{}) { 130 | wg.Done() 131 | fmt.Println(v) 132 | }, 133 | } 134 | // 添加任务函数的参数 135 | task.Params = []interface{}{i, i * 2, "hello"} 136 | // 将任务放入任务池 137 | pool.Put(task) 138 | } 139 | 140 | wg.Add(1) 141 | // 再创建一个任务 142 | pool.Put(&mortar.Task{ 143 | Handler: func(v ...interface{}) { 144 | wg.Done() 145 | fmt.Println(v) 146 | }, 147 | Params: []interface{}{"hi!"}, // 也可以在创建任务时设置参数 148 | }) 149 | 150 | wg.Wait() 151 | 152 | // 安全关闭任务池(保证已加入池中的任务被消费完) 153 | pool.Close() 154 | // 如果任务池已经关闭, Put() 方法会返回 ErrPoolAlreadyClosed 错误 155 | err = pool.Put(&mortar.Task{ 156 | Handler: func(v ...interface{}) {}, 157 | }) 158 | if err != nil { 159 | fmt.Println(err) // print: pool already closed 160 | } 161 | } 162 | 163 | ``` 164 | 165 | 更多例子参考 examples 目录下的文件 166 | 167 | 168 | ## benchmark 169 | 170 | 100w 次执行,原子增量操作 171 | 172 | 模式 | 操作时间消耗 ns/op | 内存分配大小 B/op | 内存分配次数 allocs/op 173 | -|-|-|- 174 | 原生 goroutine (100w goroutine) | 1596177880 | 103815552 | 240022 175 | 任务池开启 20 个 worker 20 goroutine) | 1378909099 | 15312 | 89 176 | 177 | ### 对比 178 | 179 | 使用任务池和原生 goroutine 性能相近(略好于原生) 180 | 181 | 使用任务池比直接 goroutine 内存分配节省 7000 倍左右, 内存分配次数减少 2700 倍左右 182 | 183 | > tips: 当任务为耗时任务时, 防止任务堆积(消费不过来)可以结合业务调整容量, 或根据业务控制每个任务的超时时间 184 | 185 | # License 186 | 187 | The Mortar is open-sourced software licensed under the [MIT license](http://opensource.org/licenses/MIT). 188 | --------------------------------------------------------------------------------