-
Notifications
You must be signed in to change notification settings - Fork 25
/
gopool.go
234 lines (212 loc) · 6.33 KB
/
gopool.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
package gopool
import (
"context"
"sort"
"sync"
"time"
)
// GoPool represents a pool of workers.
type GoPool interface {
// AddTask adds a task to the pool.
AddTask(t task)
// Wait waits for all tasks to be dispatched and completed.
Wait()
// Release releases the pool and all its workers.
Release()
// GetRunning returns the number of running workers.
Running() int
// GetWorkerCount returns the number of workers.
GetWorkerCount() int
// GetTaskQueueSize returns the size of the task queue.
GetTaskQueueSize() int
}
// task represents a function that will be executed by a worker.
// It returns a result and an error.
type task func() (interface{}, error)
// goPool represents a pool of workers.
type goPool struct {
workers []*worker
workerStack []int
maxWorkers int
// Set by WithMinWorkers(), used to adjust the number of workers. Default equals to maxWorkers.
minWorkers int
// tasks are added to this channel first, then dispatched to workers. Default buffer size is 1 million.
taskQueue chan task
// Set by WithTaskQueueSize(), used to set the size of the task queue. Default is 1e6.
taskQueueSize int
// Set by WithRetryCount(), used to retry a task when it fails. Default is 0.
retryCount int
lock sync.Locker
cond *sync.Cond
// Set by WithTimeout(), used to set a timeout for a task. Default is 0, which means no timeout.
timeout time.Duration
// Set by WithResultCallback(), used to handle the result of a task. Default is nil.
resultCallback func(interface{})
// Set by WithErrorCallback(), used to handle the error of a task. Default is nil.
errorCallback func(error)
// adjustInterval is the interval to adjust the number of workers. Default is 1 second.
adjustInterval time.Duration
ctx context.Context
// cancel is used to cancel the context. It is called when Release() is called.
cancel context.CancelFunc
}
// NewGoPool creates a new pool of workers.
func NewGoPool(maxWorkers int, opts ...Option) GoPool {
ctx, cancel := context.WithCancel(context.Background())
pool := &goPool{
maxWorkers: maxWorkers,
// Set minWorkers to maxWorkers by default
minWorkers: maxWorkers,
// workers and workerStack should be initialized after WithMinWorkers() is called
workers: nil,
workerStack: nil,
taskQueue: nil,
taskQueueSize: 1e6,
retryCount: 0,
lock: new(sync.Mutex),
timeout: 0,
adjustInterval: 1 * time.Second,
ctx: ctx,
cancel: cancel,
}
// Apply options
for _, opt := range opts {
opt(pool)
}
pool.taskQueue = make(chan task, pool.taskQueueSize)
pool.workers = make([]*worker, pool.minWorkers)
pool.workerStack = make([]int, pool.minWorkers)
if pool.cond == nil {
pool.cond = sync.NewCond(pool.lock)
}
// Create workers with the minimum number. Don't use pushWorker() here.
for i := 0; i < pool.minWorkers; i++ {
worker := newWorker()
pool.workers[i] = worker
pool.workerStack[i] = i
worker.start(pool, i)
}
go pool.adjustWorkers()
go pool.dispatch()
return pool
}
// AddTask adds a task to the pool.
func (p *goPool) AddTask(t task) {
p.taskQueue <- t
}
// Wait waits for all tasks to be dispatched and completed.
func (p *goPool) Wait() {
for {
p.lock.Lock()
workerStackLen := len(p.workerStack)
p.lock.Unlock()
if len(p.taskQueue) == 0 && workerStackLen == len(p.workers) {
break
}
time.Sleep(50 * time.Millisecond)
}
}
// Release stops all workers and releases resources.
func (p *goPool) Release() {
close(p.taskQueue)
p.cancel()
p.cond.L.Lock()
for len(p.workerStack) != p.minWorkers {
p.cond.Wait()
}
p.cond.L.Unlock()
for _, worker := range p.workers {
close(worker.taskQueue)
}
p.workers = nil
p.workerStack = nil
}
func (p *goPool) popWorker() int {
p.lock.Lock()
workerIndex := p.workerStack[len(p.workerStack)-1]
p.workerStack = p.workerStack[:len(p.workerStack)-1]
p.lock.Unlock()
return workerIndex
}
func (p *goPool) pushWorker(workerIndex int) {
p.lock.Lock()
p.workerStack = append(p.workerStack, workerIndex)
p.lock.Unlock()
p.cond.Signal()
}
// adjustWorkers adjusts the number of workers according to the number of tasks in the queue.
func (p *goPool) adjustWorkers() {
ticker := time.NewTicker(p.adjustInterval)
defer ticker.Stop()
var adjustFlag bool
for {
adjustFlag = false
select {
case <-ticker.C:
p.cond.L.Lock()
if len(p.taskQueue) > len(p.workers)*3/4 && len(p.workers) < p.maxWorkers {
adjustFlag = true
// Double the number of workers until it reaches the maximum
newWorkers := min(len(p.workers)*2, p.maxWorkers) - len(p.workers)
for i := 0; i < newWorkers; i++ {
worker := newWorker()
p.workers = append(p.workers, worker)
// Don't use len(p.workerStack)-1 here, because it will be less than len(p.workers)-1 when the pool is busy
p.workerStack = append(p.workerStack, len(p.workers)-1)
worker.start(p, len(p.workers)-1)
}
} else if len(p.taskQueue) == 0 && len(p.workerStack) == len(p.workers) && len(p.workers) > p.minWorkers {
adjustFlag = true
// Halve the number of workers until it reaches the minimum
removeWorkers := (len(p.workers) - p.minWorkers + 1) / 2
// Sort the workerStack before removing workers.
// [1,2,3,4,5] -working-> [1,2,3] -expansive-> [1,2,3,6,7] -idle-> [1,2,3,6,7,4,5]
sort.Ints(p.workerStack)
p.workers = p.workers[:len(p.workers)-removeWorkers]
p.workerStack = p.workerStack[:len(p.workerStack)-removeWorkers]
}
p.cond.L.Unlock()
if adjustFlag {
p.cond.Broadcast()
}
case <-p.ctx.Done():
return
}
}
}
// dispatch dispatches tasks to workers.
func (p *goPool) dispatch() {
for t := range p.taskQueue {
p.cond.L.Lock()
for len(p.workerStack) == 0 {
p.cond.Wait()
}
p.cond.L.Unlock()
workerIndex := p.popWorker()
p.workers[workerIndex].taskQueue <- t
}
}
func min(a, b int) int {
if a < b {
return a
}
return b
}
// Running returns the number of workers that are currently working.
func (p *goPool) Running() int {
p.lock.Lock()
defer p.lock.Unlock()
return len(p.workers) - len(p.workerStack)
}
// GetWorkerCount returns the number of workers in the pool.
func (p *goPool) GetWorkerCount() int {
p.lock.Lock()
defer p.lock.Unlock()
return len(p.workers)
}
// GetTaskQueueSize returns the size of the task queue.
func (p *goPool) GetTaskQueueSize() int {
p.lock.Lock()
defer p.lock.Unlock()
return p.taskQueueSize
}