之前寫了Go語言協程池的實踐以及動態QPS的實現,本來計劃就是開始做一些測試了,但是發現協程池的實現有些問題也有一些BUG,所以連夜修改了部分功能。
爲了不咋不明真相的讀者造成困擾,趕緊寫篇文章報告一下。
缺陷&BUG
這裏先把測試中遇到的問題和BUG梳理一下:
- 活躍協程數計算錯誤
- 執行數量和收到計數錯誤
- QPS陡增和陡降的時候,無法及時增加壓力和回收協程
- 協程回收存在問題不夠優雅,效率太低
BUG分析
活躍協程數
這裏計數錯誤的原因是因爲在原來的實現中多次使用了ReduceWorker()
和AddWorker()
方法,導致沒有將添加和減少的功能收攏到某一個時機統一處理,有兩處重複使用的問題,導致Active
計算錯誤。
執行數量和收到計數錯誤
這裏問題出現在這個方法,本意是設計一個執行固定次數任務的方法。通過將一部分任務合併到同一個任務丟給協程池。但是在實現過程中並沒有將原始的任務單獨計數。同時在記錄到所有收到的任務ReceiveTotal
中也存在這個問題。
// ExecuteQps
// @Description: 執行任務固定次數
// @receiver pool
// @param t
// @param qps
//
func (pool *GorotinesPool) ExecuteQps(t func(), qps int) {
mutiple := qps / pool.SingleTimes
remainder := qps % pool.SingleTimes
for i := 0; i < pool.SingleTimes; i++ {
pool.Execute(func() {
for i := 0; i < mutiple; i++ {
t()
}
})
}
pool.Execute(func() {
for i := 0; i < remainder; i++ {
t()
}
})
}
新建回收協程不及時
在QPS陡增陡降的場景測試中,存在2個,需要增加協程數的時候增加較慢,因爲每1秒掃描一次的等待channel
,增多增加1個,同樣的也減少1個。這個效率非常低,設計太蠢了。
線程回收不優雅
在舊實現中用的是掃描等待channel
判斷是否增加還是減少協程。除了效率低以外,還存在無法及時回收和過度回收(正在運行的協程也會收到一個終止信號)。
舊的方法內容如下(現在看簡直不忍直視):
// balance
// @Description: 平衡活躍協程數
// @receiver pool
//
func (pool *GorotinesPool) balance() {
if pool.status {
if len(pool.tasks) > 0 && pool.active < int32(pool.Max) {
pool.AddWorker()
}
if len(pool.tasks) == 0 && pool.active > int32(pool.Min) {
pool.ReduceWorker()
}
}
}
解決方法
重寫回收方法
這裏參照了Java線程池的實現,通過設置一個最大空閒時間,通過這個設置來回收協程,這樣即方便又能照顧到協程運行狀態,避免過度回收。這裏改造了一下worker()
方法。
實現內容如下:
// worker
// @Description: 開始執行協程
// @receiver pool
//
func (pool *GorotinesPool) worker() {
defer func() {
if p := recover(); p != nil {
log.Printf("execute task fail: %v", p)
}
}()
Fun:
for {
select {
case t := <-pool.tasks:
atomic.AddInt32(&pool.ExecuteTotal, 1)
t()
case <-time.After(pool.MaxIdle):
if pool.Active > int32(pool.Min) {
atomic.AddInt32(&pool.Active, -1)
break Fun
}
}
}
}
這裏順道解決了協程回收效率不高的問題。
協程增加效率
這裏做了2項修改:
- 增加掃描後單次增加協程數量,增加等待
channel
數量等量的協程數 - 調整
func (pool *GorotinesPool) Execute(t func()) error
合併任務的算法,採取固定的數量合併方案
增加協程數量:
// balance
// @Description: 平衡活躍協程數
// @receiver pool
//
func (pool *GorotinesPool) balance() {
if pool.status {
if len(pool.tasks) > 0 && pool.Active < int32(pool.Max) {
for i := 0; i < len(pool.tasks); i++ {
if int(pool.Active) < pool.Max {
pool.AddWorker()
}
}
}
}
}
調整合並任務方案:
// ExecuteQps
// @Description: 執行任務固定次數
// @receiver pool
// @param t
// @param qps
//
func (pool *GorotinesPool) ExecuteQps(t func(), qps int) {
mutiple := qps / pool.SingleTimes
remainder := qps % pool.SingleTimes
for i := 0; i < mutiple; i++ {
pool.Execute(func() {
atomic.AddInt32(&pool.ExecuteTotal, -1)
for i := 0; i < pool.SingleTimes; i++ {
atomic.AddInt32(&pool.ExecuteTotal, 1)
t()
}
})
}
pool.Execute(func() {
atomic.AddInt32(&pool.ExecuteTotal, -1)
for i := 0; i < remainder; i++ {
atomic.AddInt32(&pool.ExecuteTotal, 1)
t()
}
})
}
計數不準確
這個純屬BUG,改掉計數錯誤的地方,修復BUG,代碼已經在上述修復代碼中有所體現了。
完整代碼
package execute
import (
"errors"
"funtester/ftool" "log" "sync/atomic" "time")
type GorotinesPool struct {
Max int
Min int
tasks chan func()
status bool
Active int32
ExecuteTotal int32
SingleTimes int
addTimeout time.Duration
MaxIdle time.Duration
}
type taskType int
const (
normal taskType = 0
reduce taskType = 1
)
// GetPool
// @Description: 創建線程池
// @param max 最大協程數
// @param min 最小協程數
// @param maxWaitTask 最大任務等待長度
// @param timeout 添加任務超時時間,單位s
// @return *GorotinesPool
//
func GetPool(max, min, maxWaitTask, timeout, maxIdle int) *GorotinesPool {
p := &GorotinesPool{
Max: max,
Min: min,
tasks: make(chan func(), maxWaitTask),
status: true,
Active: 0,
ExecuteTotal: 0,
SingleTimes: 10,
addTimeout: time.Duration(timeout) * time.Second,
MaxIdle: time.Duration(maxIdle) * time.Second,
}
for i := 0; i < min; i++ {
p.AddWorker()
}
go func() {
for {
if !p.status {
break
}
ftool.Sleep(1000)
p.balance()
}
}()
return p
}
// worker
// @Description: 開始執行協程
// @receiver pool
//
func (pool *GorotinesPool) worker() {
defer func() {
if p := recover(); p != nil {
log.Printf("execute task fail: %v", p)
}
}()
Fun:
for {
select {
case t := <-pool.tasks:
atomic.AddInt32(&pool.ExecuteTotal, 1)
t()
case <-time.After(pool.MaxIdle):
if pool.Active > int32(pool.Min) {
atomic.AddInt32(&pool.Active, -1)
break Fun
}
}
}
}
// Execute
// @Description: 執行任務
// @receiver pool
// @param t
// @return error
//
func (pool *GorotinesPool) Execute(t func()) error {
if pool.status {
select {
case pool.tasks <- func() {
t()
}:
return nil
case <-time.After(pool.addTimeout):
return errors.New("add tasks timeout")
}
} else {
return errors.New("pools is down")
}
}
// Wait
// @Description: 結束等待任務完成
// @receiver pool
//
func (pool *GorotinesPool) Wait() {
pool.status = false
Fun:
for {
if len(pool.tasks) == 0 || pool.Active == 0 {
break Fun
}
ftool.Sleep(1000)
}
defer close(pool.tasks)
log.Printf("execute: %d", pool.ExecuteTotal)
}
// AddWorker
// @Description: 添加worker,協程數加1
// @receiver pool
//
func (pool *GorotinesPool) AddWorker() {
atomic.AddInt32(&pool.Active, 1)
go pool.worker()
}
// balance
// @Description: 平衡活躍協程數
// @receiver pool
//
func (pool *GorotinesPool) balance() {
if pool.status {
if len(pool.tasks) > 0 && pool.Active < int32(pool.Max) {
for i := 0; i < len(pool.tasks); i++ {
if int(pool.Active) < pool.Max {
pool.AddWorker()
}
}
}
}
}
// ExecuteQps
// @Description: 執行任務固定次數
// @receiver pool
// @param t
// @param qps
//
func (pool *GorotinesPool) ExecuteQps(t func(), qps int) {
mutiple := qps / pool.SingleTimes
remainder := qps % pool.SingleTimes
for i := 0; i < mutiple; i++ {
pool.Execute(func() {
atomic.AddInt32(&pool.ExecuteTotal, -1)
for i := 0; i < pool.SingleTimes; i++ {
atomic.AddInt32(&pool.ExecuteTotal, 1)
t()
}
})
}
pool.Execute(func() {
atomic.AddInt32(&pool.ExecuteTotal, -1)
for i := 0; i < remainder; i++ {
atomic.AddInt32(&pool.ExecuteTotal, 1)
t()
}
})
}