• Stars
    star
    130
  • Rank 277,575 (Top 6 %)
  • Language
    Go
  • License
    MIT License
  • Created about 5 years ago
  • Updated over 3 years ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

A goroutine pool

Mortar

A goroutine task pool

一个简单好用的高性能任务池, 代码只有 100 多行。

版本更新日志

v1.x

v1.1

  • 部分冗余逻辑优化

v1.2

  • 修复数据竞争 bug

v1.3

  • 安全运行 worker

v1.4

  • 退出等待 taskC 清空时增加 sleep 减少 cpu 负载

v1.5

  • 优化锁,解决只有一个 woker 时产生 panic 后无发消费 task 导致 deadlock 的问题 (见 issue 极端情况 #4)

解决什么问题

go 的 goroutine 提供了一种较线程而言更廉价的方式处理并发场景, 但 goroutine 太多会导致调度性能下降、GC 频繁、内存暴涨, 引发一系列问题。

mortar 限制了最多可启动的 goroutine 数量, 同时保持和原生 goroutine 一样的性能(在海量 goroutine 场景优于原生 goroutine), 避免了上述问题。

原理

创建一个容量为 N 的池, 在池容量未满时, 每塞入一个任务(生产任务), 任务池开启一个 worker (建立协程) 去处理任务(消费任务)。 当任务池容量赛满,每塞入一个任务(生产任务), 任务会被已有的 N 个 worker 抢占执行(消费任务),达到协程限制的功能。

生产消费模型

队列: channel

生产任务: 将任务写入 channel

消费任务: worker(goroutine)从 channel 中读出任务执行

使用

task struct

每个任务是一个结构体, Handler 是要执行的任务函数, Params 是要传入 Handler 的参数

type Task struct {
	Handler func(v ...interface{})
	Params  []interface{}
}

NewPool

NewPool() 方法创建一个任务池结构, 返回其指针

func NewPool(capacity uint64) (*Pool, error)

Put

Put() 方法来将一个任务放入池中, 如果任务池未满, 则启动一个 worker。

func (p *Pool) Put(task *Task) error 

GetCap

获取任务池容量, 创建任务池时已确定

func (p *Pool) GetCap() uint64

GetRunningWorkers

获取当前运行 worker 的数量

func (p *Pool) GetRunningWorkers() uint64 

Close()

安全关闭任务池。Close() 方法会先阻止 Put() 方法继续放入任务, 等待所有任务都被消费运行后, 销毁所有 worker, 关闭任务 channel。

func (p *Pool) Close() 

panic handler

每个 worker 都是一个原生 goroutine, 为保证程序的安全运行, 任务池会 recover 所有 worker 中的 panic, 并提供自定义的 panic 处理能力(不设置 PanicHandler 默认会打印 panic 的异常栈)。

pool.PanicHandler = func(r interface{}) {
	// handle panic
	log.Println(r) 
}

安装

go get github.com/wazsmwazsm/mortar

例子

package main

import (
	"fmt"
	"github.com/wazsmwazsm/mortar"
	"sync"
)

func main() {
	// 创建容量为 10 的任务池
	pool, err := mortar.NewPool(10)
	if err != nil {
		panic(err)
	}

	wg := new(sync.WaitGroup)

	for i := 0; i < 1000; i++ {
		wg.Add(1)
		// 创建任务
		task := &mortar.Task{
			Handler: func(v ...interface{}) {
				wg.Done()
				fmt.Println(v)
			},
		}
		// 添加任务函数的参数
		task.Params = []interface{}{i, i * 2, "hello"}
		// 将任务放入任务池
		pool.Put(task)
	}

	wg.Add(1)
	// 再创建一个任务
	pool.Put(&mortar.Task{
		Handler: func(v ...interface{}) {
			wg.Done()
			fmt.Println(v)
		},
		Params: []interface{}{"hi!"}, // 也可以在创建任务时设置参数
	})

	wg.Wait()

	// 安全关闭任务池(保证已加入池中的任务被消费完)
	pool.Close()
	// 如果任务池已经关闭, Put() 方法会返回 ErrPoolAlreadyClosed 错误
	err = pool.Put(&mortar.Task{
		Handler: func(v ...interface{}) {},
	})
	if err != nil {
		fmt.Println(err) // print: pool already closed
	}
}

更多例子参考 examples 目录下的文件

benchmark

100w 次执行,原子增量操作

模式 操作时间消耗 ns/op 内存分配大小 B/op 内存分配次数 allocs/op
原生 goroutine (100w goroutine) 1596177880 103815552 240022
任务池开启 20 个 worker 20 goroutine) 1378909099 15312 89

对比

使用任务池和原生 goroutine 性能相近(略好于原生)

使用任务池比直接 goroutine 内存分配节省 7000 倍左右, 内存分配次数减少 2700 倍左右

tips: 当任务为耗时任务时, 防止任务堆积(消费不过来)可以结合业务调整容量, 或根据业务控制每个任务的超时时间

License

The Mortar is open-sourced software licensed under the MIT license.