add loop queue (#53)

* add loop queue

* add loop queue

* fix the bugs

add loop queue

move the worker queue to directory

按照新的接口实现 lifo 队列

添加新接口的环形队列实现

rename the slice queue

修复了 unlock

使用 queue 管理 goWorkerWithFunc

使用 dequeue 判断队列

add remainder

增加测试文件

循环队列需要一个空闲位

* remove interface{}

* Refine the logic of sync.Pool

* Add flowcharts of ants into READMEs

* Add the installation about ants v2

* Renew the functional options in READMEs

* Renew English and Chinese flowcharts

* rename package name

移动 worker queue 位置

worker queue 都修改为私有接口

考虑到性能问题,把 interface{} 改回到  *goworker

* 修改 releaseExpiry 和 releaseAll

* remove files

* fix some bug
This commit is contained in:
Kevin Bai 2019-10-10 00:59:19 +08:00 committed by Andy Pan
parent 0efbda3f68
commit f0e23928f4
6 changed files with 493 additions and 39 deletions

62
pool.go
View File

@ -42,7 +42,7 @@ type Pool struct {
expiryDuration time.Duration expiryDuration time.Duration
// workers is a slice that store the available workers. // workers is a slice that store the available workers.
workers []*goWorker workers workerQueue
// release is used to notice the pool to closed itself. // release is used to notice the pool to closed itself.
release int32 release int32
@ -82,35 +82,21 @@ func (p *Pool) periodicallyPurge() {
heartbeat := time.NewTicker(p.expiryDuration) heartbeat := time.NewTicker(p.expiryDuration)
defer heartbeat.Stop() defer heartbeat.Stop()
var expiredWorkers []*goWorker
for range heartbeat.C { for range heartbeat.C {
if atomic.LoadInt32(&p.release) == CLOSED { if atomic.LoadInt32(&p.release) == CLOSED {
break break
} }
currentTime := time.Now()
p.lock.Lock() p.lock.Lock()
idleWorkers := p.workers stream := p.workers.releaseExpiry(p.expiryDuration)
n := len(idleWorkers)
var i int
for i = 0; i < n && currentTime.Sub(idleWorkers[i].recycleTime) > p.expiryDuration; i++ {
}
expiredWorkers = append(expiredWorkers[:0], idleWorkers[:i]...)
if i > 0 {
m := copy(idleWorkers, idleWorkers[i:])
for i = m; i < n; i++ {
idleWorkers[i] = nil
}
p.workers = idleWorkers[:m]
}
p.lock.Unlock() p.lock.Unlock()
// Notify obsolete workers to stop. // Notify obsolete workers to stop.
// This notification must be outside the p.lock, since w.task // This notification must be outside the p.lock, since w.task
// may be blocking and may consume a lot of time if many workers // may be blocking and may consume a lot of time if many workers
// are located on non-local CPUs. // are located on non-local CPUs.
for i, w := range expiredWorkers { for w := range stream {
w.task <- nil w.task <- nil
expiredWorkers[i] = nil
} }
// There might be a situation that all workers have been cleaned up(no any worker is running) // There might be a situation that all workers have been cleaned up(no any worker is running)
@ -156,8 +142,11 @@ func NewPool(size int, options ...Option) (*Pool, error) {
}, },
} }
if opts.PreAlloc { if opts.PreAlloc {
p.workers = make([]*goWorker, 0, size) p.workers = newQueue(loopQueueType, size)
} else {
p.workers = newQueue(stackType, 0)
} }
p.cond = sync.NewCond(p.lock) p.cond = sync.NewCond(p.lock)
// Start a goroutine to clean up expired workers periodically. // Start a goroutine to clean up expired workers periodically.
@ -166,7 +155,7 @@ func NewPool(size int, options ...Option) (*Pool, error) {
return p, nil return p, nil
} }
//--------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Submit submits a task to this pool. // Submit submits a task to this pool.
func (p *Pool) Submit(task func()) error { func (p *Pool) Submit(task func()) error {
@ -209,17 +198,12 @@ func (p *Pool) Release() {
p.once.Do(func() { p.once.Do(func() {
atomic.StoreInt32(&p.release, 1) atomic.StoreInt32(&p.release, 1)
p.lock.Lock() p.lock.Lock()
idleWorkers := p.workers p.workers.releaseAll()
for i, w := range idleWorkers {
w.task <- nil
idleWorkers[i] = nil
}
p.workers = nil
p.lock.Unlock() p.lock.Unlock()
}) })
} }
//--------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// incRunning increases the number of the currently running goroutines. // incRunning increases the number of the currently running goroutines.
func (p *Pool) incRunning() { func (p *Pool) incRunning() {
@ -240,12 +224,9 @@ func (p *Pool) retrieveWorker() *goWorker {
} }
p.lock.Lock() p.lock.Lock()
idleWorkers := p.workers
n := len(idleWorkers) - 1 w = p.workers.dequeue()
if n >= 0 { if w != nil {
w = idleWorkers[n]
idleWorkers[n] = nil
p.workers = idleWorkers[:n]
p.lock.Unlock() p.lock.Unlock()
} else if p.Running() < p.Cap() { } else if p.Running() < p.Cap() {
p.lock.Unlock() p.lock.Unlock()
@ -268,13 +249,12 @@ func (p *Pool) retrieveWorker() *goWorker {
spawnWorker() spawnWorker()
return w return w
} }
l := len(p.workers) - 1
if l < 0 { w = p.workers.dequeue()
if w == nil {
goto Reentry goto Reentry
} }
w = p.workers[l]
p.workers[l] = nil
p.workers = p.workers[:l]
p.lock.Unlock() p.lock.Unlock()
} }
return w return w
@ -287,7 +267,11 @@ func (p *Pool) revertWorker(worker *goWorker) bool {
} }
worker.recycleTime = time.Now() worker.recycleTime = time.Now()
p.lock.Lock() p.lock.Lock()
p.workers = append(p.workers, worker)
err := p.workers.enqueue(worker)
if err != nil {
return false
}
// Notify the invoker stuck in 'retrieveWorker()' of there is an available worker in the worker queue. // Notify the invoker stuck in 'retrieveWorker()' of there is an available worker in the worker queue.
p.cond.Signal() p.cond.Signal()

137
worker_loop_queue.go Normal file
View File

@ -0,0 +1,137 @@
package ants
import "time"
type loopQueue struct {
items []*goWorker
expiry []*goWorker
head int
tail int
remainder int
}
func newLoopQueue(size int) *loopQueue {
if size <= 0 {
return nil
}
wq := loopQueue{
items: make([]*goWorker, size+1),
expiry: make([]*goWorker, 0),
head: 0,
tail: 0,
remainder: size + 1,
}
return &wq
}
func (wq *loopQueue) len() int {
if wq.remainder == 0 {
return 0
}
return (wq.tail - wq.head + wq.remainder) % wq.remainder
}
func (wq *loopQueue) cap() int {
if wq.remainder == 0 {
return 0
}
return wq.remainder - 1
}
func (wq *loopQueue) isEmpty() bool {
return wq.tail == wq.head
}
func (wq *loopQueue) enqueue(worker *goWorker) error {
if wq.remainder == 0 {
return ErrQueueLengthIsZero
}
if (wq.tail+1)%wq.remainder == wq.head {
return ErrQueueIsFull
}
wq.items[wq.tail] = worker
wq.tail = (wq.tail + 1) % wq.remainder
return nil
}
func (wq *loopQueue) dequeue() *goWorker {
if wq.len() == 0 {
return nil
}
w := wq.items[wq.head]
wq.head = (wq.head + 1) % wq.remainder
return w
}
func (wq *loopQueue) releaseExpiry(duration time.Duration) chan *goWorker {
stream := make(chan *goWorker)
if wq.len() == 0 {
close(stream)
return stream
}
wq.expiry = wq.expiry[:0]
expiryTime := time.Now().Add(-duration)
for wq.head != wq.tail {
if expiryTime.After(wq.items[wq.head].recycleTime) {
wq.expiry = append(wq.expiry, wq.items[wq.head])
wq.head = (wq.head + 1) % wq.remainder
continue
}
break
}
go func() {
defer close(stream)
for i := 0; i < len(wq.expiry); i++ {
stream <- wq.expiry[i]
}
}()
return stream
}
//func (wq *LoopQueue)search(compareTime time.Time, l, r int) int {
// if l == r {
// if wq.items[l].recycleTime.After(compareTime) {
// return -1
// } else {
// return l
// }
// }
//
// c := cap(wq.items)
// mid := ((r-l+c)/2 + l) % c
// if mid == l {
// return wq.search(compareTime, l, l)
// } else if wq.items[mid].recycleTime.After(compareTime) {
// return wq.search(compareTime, l, mid-1)
// } else {
// return wq.search(compareTime, mid+1, r)
// }
//}
func (wq *loopQueue) releaseAll() {
if wq.len() == 0 {
return
}
for wq.head != wq.tail {
wq.items[wq.head].task <- nil
wq.head = (wq.head + 1) % wq.remainder
}
wq.items = wq.items[:0]
wq.remainder = 0
wq.head = 0
wq.tail = 0
}

73
worker_loop_queue_test.go Normal file
View File

@ -0,0 +1,73 @@
package ants
import (
"testing"
"time"
)
func TestNewLoopQueue(t *testing.T) {
size := 100
q := newLoopQueue(size)
if q.len() != 0 {
t.Fatalf("Len error")
}
if q.cap() != size {
t.Fatalf("Cap error")
}
if !q.isEmpty() {
t.Fatalf("IsEmpty error")
}
if q.dequeue() != nil {
t.Fatalf("Dequeue error")
}
}
func TestLoopQueue(t *testing.T) {
size := 10
q := newLoopQueue(size)
for i := 0; i < 5; i++ {
err := q.enqueue(&goWorker{recycleTime: time.Now()})
if err != nil {
break
}
}
if q.len() != 5 {
t.Fatalf("Len error")
}
v := q.dequeue()
t.Log(v)
if q.len() != 4 {
t.Fatalf("Len error")
}
time.Sleep(time.Second)
for i := 0; i < 6; i++ {
err := q.enqueue(&goWorker{recycleTime: time.Now()})
if err != nil {
break
}
}
if q.len() != 10 {
t.Fatalf("Len error")
}
err := q.enqueue(&goWorker{recycleTime: time.Now()})
if err == nil {
t.Fatalf("Enqueue error")
}
q.releaseExpiry(time.Second)
if q.len() != 6 {
t.Fatalf("Len error: %d", q.len())
}
}

39
worker_queue.go Normal file
View File

@ -0,0 +1,39 @@
package ants
import (
"errors"
"time"
)
var (
ErrQueueIsFull = errors.New("the queue is full")
ErrQueueLengthIsZero = errors.New("the queue length is zero")
)
type workerQueue interface {
len() int
cap() int
isEmpty() bool
enqueue(worker *goWorker) error
dequeue() *goWorker
releaseExpiry(duration time.Duration) chan *goWorker
releaseAll()
}
type queueType int
const (
stackType queueType = 1 << iota
loopQueueType
)
func newQueue(qType queueType, size int) workerQueue {
switch qType {
case stackType:
return newWorkerStack(size)
case loopQueueType:
return newLoopQueue(size)
default:
return newWorkerStack(size)
}
}

99
worker_stack.go Normal file
View File

@ -0,0 +1,99 @@
package ants
import "time"
type workerStack struct {
items []*goWorker
expiry []*goWorker
}
func newWorkerStack(size int) *workerStack {
if size < 0 {
return nil
}
wq := workerStack{
items: make([]*goWorker, 0, size),
expiry: make([]*goWorker, 0),
}
return &wq
}
func (wq *workerStack) len() int {
return len(wq.items)
}
func (wq *workerStack) cap() int {
return cap(wq.items)
}
func (wq *workerStack) isEmpty() bool {
return len(wq.items) == 0
}
func (wq *workerStack) enqueue(worker *goWorker) error {
wq.items = append(wq.items, worker)
return nil
}
func (wq *workerStack) dequeue() *goWorker {
l := wq.len()
if l == 0 {
return nil
}
w := wq.items[l-1]
wq.items = wq.items[:l-1]
return w
}
func (wq *workerStack) releaseExpiry(duration time.Duration) chan *goWorker {
stream := make(chan *goWorker)
n := wq.len()
if n == 0 {
close(stream)
return stream
}
expiryTime := time.Now().Add(-duration)
index := wq.search(0, n-1, expiryTime)
wq.expiry = wq.expiry[:0]
if index != -1 {
wq.expiry = append(wq.expiry, wq.items[:index+1]...)
m := copy(wq.items, wq.items[index+1:])
wq.items = wq.items[:m]
}
go func() {
defer close(stream)
for i := 0; i < len(wq.expiry); i++ {
stream <- wq.expiry[i]
}
}()
return stream
}
func (wq *workerStack) search(l, r int, expiryTime time.Time) int {
var mid int
for l <= r {
mid = (l + r) / 2
if expiryTime.Before(wq.items[mid].recycleTime) {
r = mid - 1
} else {
l = mid + 1
}
}
return r
}
func (wq *workerStack) releaseAll() {
for i := 0; i < wq.len(); i++ {
wq.items[i].task <- nil
}
wq.items = wq.items[:0]
}

122
worker_stack_test.go Normal file
View File

@ -0,0 +1,122 @@
package ants
import (
"testing"
"time"
)
func TestNewWorkerStack(t *testing.T) {
size := 100
q := newWorkerStack(size)
if q.len() != 0 {
t.Fatalf("Len error")
}
if q.cap() != size {
t.Fatalf("Cap error")
}
if !q.isEmpty() {
t.Fatalf("IsEmpty error")
}
if q.dequeue() != nil {
t.Fatalf("Dequeue error")
}
}
func TestWorkerStack(t *testing.T) {
q := newWorkerStack(0)
for i := 0; i < 5; i++ {
err := q.enqueue(&goWorker{recycleTime: time.Now()})
if err != nil {
break
}
}
if q.len() != 5 {
t.Fatalf("Len error")
}
expired := time.Now()
err := q.enqueue(&goWorker{recycleTime: expired})
if err != nil {
t.Fatalf("Enqueue error")
}
time.Sleep(time.Second)
for i := 0; i < 6; i++ {
err := q.enqueue(&goWorker{recycleTime: time.Now()})
if err != nil {
t.Fatalf("Enqueue error")
}
}
if q.len() != 12 {
t.Fatalf("Len error")
}
q.releaseExpiry(time.Second)
if q.len() != 6 {
t.Fatalf("Len error")
}
}
func TestSearch(t *testing.T) {
q := newWorkerStack(0)
// 1
expiry1 := time.Now()
_ = q.enqueue(&goWorker{recycleTime: time.Now()})
index := q.search(0, q.len()-1, time.Now())
if index != 0 {
t.Fatalf("should is 0")
}
index = q.search(0, q.len()-1, expiry1)
if index != -1 {
t.Fatalf("should is -1")
}
// 2
expiry2 := time.Now()
_ = q.enqueue(&goWorker{recycleTime: time.Now()})
index = q.search(0, q.len()-1, expiry1)
if index != -1 {
t.Fatalf("should is -1")
}
index = q.search(0, q.len()-1, expiry2)
if index != 0 {
t.Fatalf("should is 0")
}
index = q.search(0, q.len()-1, time.Now())
if index != 1 {
t.Fatalf("should is 1")
}
// more
for i := 0; i < 5; i++ {
_ = q.enqueue(&goWorker{recycleTime: time.Now()})
}
expiry3 := time.Now()
_ = q.enqueue(&goWorker{recycleTime: expiry3})
for i := 0; i < 10; i++ {
_ = q.enqueue(&goWorker{recycleTime: time.Now()})
}
index = q.search(0, q.len()-1, expiry3)
if index != 7 {
t.Fatalf("should is 7")
}
}