Introduction
Following the previous ANTS a library of Go daily , in this article we take a look at ants
source.
Pool
From the previous article, we know that there are two ways to create the ants
p, _ := ants.NewPool(cap)
p.Submit(task)
object created in this way needs to call 060c2a083ca8f0 to submit the task. The task is a function with no parameters and no return value;p, _ := ants.NewPoolWithFunc(cap, func(interface{}))
object created in this way needs to specify the pool function, and usep.Invoke(arg)
call the pool function.arg
is the parameter passed to the pool functionfunc(interface{})
In ants
, these two pools are represented by different structures: ants.Pool
and ants.PoolWithFunc
. Let's first introduce Pool
. PoolWithFunc
structure of 060c2a083ca9e8 is also similar. After introducing Pool
, let's compare them briefly.
Pool
structure of 060c2a083caa1e is defined in file pool.go
:
// src/github.com/panjf2000/ants/pool.go
type Pool struct {
capacity int32
running int32
workers workerArray
state int32
lock sync.Locker
cond *sync.Cond
workerCache sync.Pool
blockingNum int
options *Options
}
The meaning of each field is as follows:
capacity
: pool capacity, indicating the maximum number of goroutinesants
If it is a negative number, it means that the capacity is unlimited;running
: The number of worker goroutines that have been created;workers
: Store a group of worker objects,workerArray
is just an interface, which represents a worker container, which will be described in detail later;state
: Record the current status of the pool and whether it is closed (CLOSED
);lock
: lock.ants
implements a spin lock by itself. Used to synchronize concurrent operations;cond
: Condition variables. Processing task waiting and waking up;workerCache
: Usesync.Pool
object pool to manage and createworker
objects to improve performance;blockingNum
: the number of tasks blocked waiting;options
: Option. The previous article has already introduced it in detail.
Here is a clear concept. In ants
, each task is processed by a worker object, and each worker object will create a corresponding goroutine to process the task. ants
is used in goWorker
represent worker:
// src/github.com/panjf2000/ants/worker.go
type goWorker struct {
pool *Pool
task chan func()
recycleTime time.Time
}
This section will be described in detail later. Now we only need to know that the Pool.workers
field is the container for goWorker
Pool
To create a Pool
object, you need to call the ants.NewPool(size, options)
function. Some code for processing options is omitted, and the final code is as follows:
// src/github.com/panjf2000/ants/pool.go
func NewPool(size int, options ...Option) (*Pool, error) {
// ...
p := &Pool{
capacity: int32(size),
lock: internal.NewSpinLock(),
options: opts,
}
p.workerCache.New = func() interface{} {
return &goWorker{
pool: p,
task: make(chan func(), workerChanCap),
}
}
if p.options.PreAlloc {
if size == -1 {
return nil, ErrInvalidPreAllocSize
}
p.workers = newWorkerArray(loopQueueType, size)
} else {
p.workers = newWorkerArray(stackType, 0)
}
p.cond = sync.NewCond(p.lock)
go p.purgePeriodically()
return p, nil
}
The code is not difficult to understand:
- Create a
Pool
object, set the capacity, create a spin lock to initialize thelock
field, and set options; - Set
workerCache
thissync.Pool
objectNew
method, callingsync.Pool
objectGet()
time method, if it is not cached worker object, and then call this method to create a; - According to whether the pre-allocation option is set, different types of workers are created;
- Use the
p.lock
lock to create a condition variable; - Finally, start a goroutine to periodically clean up expired workers.
Pool.workers
field is of workerArray
, which is actually an interface, representing a worker container:
type workerArray interface {
len() int
isEmpty() bool
insert(worker *goWorker) error
detach() *goWorker
retrieveExpiry(duration time.Duration) []*goWorker
reset()
}
Each method has a good understanding of the meaning from the name:
len() int
: the number of workers;isEmpty() bool
: Whether the number of workers is 0;insert(worker *goWorker) error
: After the goroutine task is executed, put the corresponding worker back intoworkerArray
;detach() *goWorker
: Take out a workerworkerArray
retrieveExpiry(duration time.Duration) []*goWorker
: Remove all expired workers;reset()
: Reset the container.
workerArray
has two implementations in ants
workerStack
and loopQueue
.
workerStack
Let's first introduce workerStack
, which is located in the file worker_stack.go
:
// src/github.com/panjf2000/ants/worker_stack.go
type workerStack struct {
items []*goWorker
expiry []*goWorker
size int
}
func newWorkerStack(size int) *workerStack {
return &workerStack{
items: make([]*goWorker, 0, size),
size: size,
}
}
items
: freeworker
;expiry
: Expiredworker
.
After the goroutine completes the task, the Pool
pool will return the corresponding worker to workerStack
, and call workerStack.insert()
directly from append
to items
:
func (wq *workerStack) insert(worker *goWorker) error {
wq.items = append(wq.items, worker)
return nil
}
When a new task arrives, it will call workerStack.detach()
to retrieve an idle worker from the container:
func (wq *workerStack) detach() *goWorker {
l := wq.len()
if l == 0 {
return nil
}
w := wq.items[l-1]
wq.items[l-1] = nil // avoid memory leaks
wq.items = wq.items[:l-1]
return w
}
The last worker is always returned here, and each time insert()
is also append
to the end, which conforms to the characteristics of the stack, so it is called workerStack
.
There is a detail here. Since the underlying structure of the slice is an array, as long as there is a pointer to the array, the elements in the array will not be released. After the last element of the slice is taken out, the pointer of the corresponding array element is set to nil
, and the reference is actively released.
As mentioned above Pool
object is created, a goroutine will be created to periodically check and clean up expired workers. Get the list of expired workers by calling workerArray.retrieveExpiry()
workerStack
implemented as follows:
func (wq *workerStack) retrieveExpiry(duration time.Duration) []*goWorker {
n := wq.len()
if n == 0 {
return nil
}
expiryTime := time.Now().Add(-duration)
index := wq.binarySearch(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:])
for i := m; i < n; i++ {
wq.items[i] = nil
}
wq.items = wq.items[:m]
}
return wq.expiry
}
The implementation uses binary search to find the nearest worker that has expired. Since the expiration time is calculated according to the idle time after the goroutine executes the task, and the workerStack.insert()
enqueue order is determined, their expiration time is from early to late. So you can use binary search:
func (wq *workerStack) binarySearch(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
}
The binary search is for the worker that has expired recently, and the one before the worker that is about to expire. It and the workers before it have all expired.
If you find an index index
, the items
from beginning to index
all worker (including) copied to expiry
field. Then index
to the slice header, where the copy
function is used. copy
returns the actual number of copies, that is, the number of unexpired workers m
. Then set all the elements of the items
starting from m
nil
to avoid memory leaks because they have been copied to the head. Finally, cut the items
slice, and return the expired worker slice.
loopQueue
loopQueue
based on a circular queue, and the structure is defined in the file worker_loop_queue
:
type loopQueue struct {
items []*goWorker
expiry []*goWorker
head int
tail int
size int
isFull bool
}
func newWorkerLoopQueue(size int) *loopQueue {
return &loopQueue{
items: make([]*goWorker, size),
size: size,
}
}
Since it is a circular queue, a slice size
The circular queue has a queue head pointer head
, which points to the first position with an element, and a queue tail pointer tail
, which points to the next position where the element can be stored. So the initial state is as follows:
Add an element at tail
tail
pointer moves back. Take out the element at head
, after taking out the head
pointer also moves back. After a period of operation, the queue status is as follows:
head
or tail
end of the queue and needs to wrap around. So this situation may occur:
When the tail
pointer catches up with the head
pointer, the queue is full:
When the head
pointer catches up with the tail
pointer, the queue is empty again:
According to the schematic diagram, let's look loopQueue
the operation method of 060c2a083cb689 is very simple.
Since head
and tail
are equal there may be a queue is empty, also possible that the queue is full, so loopQueue
added a isFull
field to show the distinction. After the goroutine completes the task, it will put the corresponding worker object back to loopQueue
, and execute the insert()
method:
func (wq *loopQueue) insert(worker *goWorker) error {
if wq.size == 0 {
return errQueueIsReleased
}
if wq.isFull {
return errQueueIsFull
}
wq.items[wq.tail] = worker
wq.tail++
if wq.tail == wq.size {
wq.tail = 0
}
if wq.tail == wq.head {
wq.isFull = true
}
return nil
}
This method executes the enqueue process of the circular queue. Note that if tail==head
is inserted after insertion, the queue is full. Set the isFull
field.
The arrival of a new task calls the loopQueeue.detach()
method to obtain an idle worker structure:
func (wq *loopQueue) detach() *goWorker {
if wq.isEmpty() {
return nil
}
w := wq.items[wq.head]
wq.items[wq.head] = nil
wq.head++
if wq.head == wq.size {
wq.head = 0
}
wq.isFull = false
return w
}
isFull
process of the circular queue. Note that after each dequeue, the queue is definitely not full, and 060c2a083cb746 should be reset to false
.
workerStack
structure of 060c2a083cb771, the first entry worker object expires early and the last entry is late. The method of obtaining expired workers workerStack
, except that binary search is not used. I won't go into details here.
Look at the creation of Pool
End introduce two workerArray
after implementation, look Pool
create a function in workers
field is set:
if p.options.PreAlloc {
if size == -1 {
return nil, ErrInvalidPreAllocSize
}
p.workers = newWorkerArray(loopQueueType, size)
} else {
p.workers = newWorkerArray(stackType, 0)
}
newWorkerArray()
defined in the file worker_array.go
:
type arrayType int
const (
stackType arrayType = 1 << iota
loopQueueType
)
func newWorkerArray(aType arrayType, size int) workerArray {
switch aType {
case stackType:
return newWorkerStack(size)
case loopQueueType:
return newWorkerLoopQueue(size)
default:
return newWorkerStack(size)
}
}
That is, if the pre-allocation option is set, the loopQueue
structure is used. Otherwise, use the structure of stack
worker structure
After introducing Pool
, let's take a look at the structure of the worker. In ants
, the worker is represented by the structure goWorker
, which is defined in the file worker.go
. Its structure is very simple:
// src/github.com/panjf2000/ants/worker.go
type goWorker struct {
pool *Pool
task chan func()
recycleTime time.Time
}
The meaning of the specific fields is obvious:
pool
: Holds a reference to the goroutine pool;task
: Task channel, through this channel, the function offunc ()
goWorker
as a task;recyleTime
: This field recordsgoWorker
is put back into the pool (that is, when it starts to be idle). After it completes the task, it is set when it is put back into the goroutine pool.
goWorker
created, the run()
method will be called, and run()
method to process the task. run()
main process of 060c2a083cb980 is very simple:
func (w *goWorker) run() {
go func() {
for f := range w.task {
if f == nil {
return
}
f()
if ok := w.pool.revertWorker(w); !ok {
return
}
}
}()
}
This method starts a new goroutine, and then continuously task
channel, and then executes the task. After the task is executed, the revertWorker()
method of the goWorker
object back into the pool so that it can be taken out to process a new task next time . revertWorker()
method will be analyzed in detail later.
Note here that, in fact for f := range w.task
this cycle until the channel task
off or removed as nil
task will be terminated. So this goroutine has been running, which is the key to the high performance of ants
Each goWorker
will only start the goroutine once, and reuse this goroutine later. The goroutine will be put back into the pool every time it executes only one task.
There is one detail, if returned to operation fails, it will call return
, ending it makes goroutine run, goroutine prevent leakage .
Here f == nil
is true, return
is also a detailed point. We will introduce it in detail later when we talk about pool closure.
Let's take a look at the exception handling of the run()
defer func() {
w.pool.workerCache.Put(w)
if p := recover(); p != nil {
if ph := w.pool.options.PanicHandler; ph != nil {
ph(p)
} else {
w.pool.options.Logger.Printf("worker exits from a panic: %v\n", p)
var buf [4096]byte
n := runtime.Stack(buf[:], false)
w.pool.options.Logger.Printf("worker exits from panic: %s\n", string(buf[:n]))
}
}
w.pool.cond.Signal()
}()
To put it simply, in defer
, the recover()
function is used to capture the panic
thrown during the execution of the task. At this time, the task execution failed and the goroutine ended. But the goWorker
object can still be reused, so the defer
function calls w.pool.workerCache.Put(w)
beginning to put the goWorker
object back into the sync.Pool
pool.
The next step is to process panic
panic
processor is specified in the options, call this processor directly. Otherwise, ants
call options set Logger
record some logs, such as the stack, panic
information.
Finally, you need to call w.pool.cond.Signal()
notify that there is now a free goWorker
. Because we actually run goWorker
amount due panic
one less, but the pool may have other tasks waiting to be processed.
Submit task
Next, the entire process can be chained up by submitting tasks. From the previous article, we know that you can call the Submit()
method of the pool object to submit a task:
func (p *Pool) Submit(task func()) error {
if p.IsClosed() {
return ErrPoolClosed
}
var w *goWorker
if w = p.retrieveWorker(); w == nil {
return ErrPoolOverload
}
w.task <- task
return nil
}
First determine whether the pool is closed, then call the retrieveWorker()
method to obtain an idle worker, and then send the task task
to the task channel of the worker. The following is the implementation retrieveWorker()
func (p *Pool) retrieveWorker() (w *goWorker) {
p.lock.Lock()
w = p.workers.detach()
if w != nil {
p.lock.Unlock()
} else if capacity := p.Cap(); capacity == -1 || capacity > p.Running() {
p.lock.Unlock()
spawnWorker()
} else {
if p.options.Nonblocking {
p.lock.Unlock()
return
}
Reentry:
if p.options.MaxBlockingTasks != 0 && p.blockingNum >= p.options.MaxBlockingTasks {
p.lock.Unlock()
return
}
p.blockingNum++
p.cond.Wait()
p.blockingNum--
var nw int
if nw = p.Running(); nw == 0 {
p.lock.Unlock()
if !p.IsClosed() {
spawnWorker()
}
return
}
if w = p.workers.detach(); w == nil {
if nw < capacity {
p.lock.Unlock()
spawnWorker()
return
}
goto Reentry
}
p.lock.Unlock()
}
return
}
This method is a bit more complicated, let's look at it a little bit. First call p.workers.detach()
obtain the goWorker
object. p.workers
is a loopQueue
or workerStack
object, they all implement the detach()
method, which has been introduced before.
If a goWorker
object is returned, indicating that there is an idle goroutine, return directly.
Otherwise, the pool capacity has not been used up (that is, the capacity is greater than the goWorker
), call spawnWorker()
create a new goWorker
, and execute its run()
method:
spawnWorker := func() {
w = p.workerCache.Get().(*goWorker)
w.run()
}
Otherwise, the pool capacity has been used up. If the non-blocking option is set, return directly. Otherwise, if the upper limit of the maximum blocking queue length is set, and the number of currently blocked waiting tasks has reached this upper limit, return directly. Otherwise, the number of blocked waits is +1, and p.cond.Wait()
called to wait.
Then after goWorker.run()
completes a task, call the revertWorker()
method of the goWorker
:
func (p *Pool) revertWorker(worker *goWorker) bool {
if capacity := p.Cap(); (capacity > 0 && p.Running() > capacity) || p.IsClosed() {
return false
}
worker.recycleTime = time.Now()
p.lock.Lock()
if p.IsClosed() {
p.lock.Unlock()
return false
}
err := p.workers.insert(worker)
if err != nil {
p.lock.Unlock()
return false
}
p.cond.Signal()
p.lock.Unlock()
return true
}
Here provided goWorker
of recycleTime
field, for determining expired. Then put goWorker
back into the pool. workers
of insert()
front approach has also been analyzed before.
Then call p.cond.Signal()
wake up the wait in the retrieveWorker()
retrieveWorker()
method continues to execute, and the number of blocking waits is -1. Here, the current goWorker
(that is, the number of goroutines) is judged. If the number is equal to 0, it is very likely that the pool has just executed Release()
shutdown. At this time, it is necessary to determine whether the pool is in the closed state, and if so, return directly. Otherwise, call spawnWorker()
create a new goWorker
and execute its run()
method.
If the current goWorker
is not 0, call p.workers.detach()
take out an idle goWorker
return. This operation may fail because there may be multiple goroutines waiting at the same time, and only some goroutines can get goWorker
when waking up. If it fails, and its capacity has not been used up, create a new goWorker
directly, otherwise re-execute the blocking waiting logic.
There are a lot of locking and unlocking logics, and it's hard to read and understand mixed with the semaphore. In fact, it is very simple to know only one point, that is, p.cond.Wait()
will internally suspend the current goroutine, and then unlock the lock it holds, which will call p.lock.Unlock()
. This is also the reason why revertWorker()
can be successfully locked in p.lock.Lock()
Then p.cond.Signal()
or p.cond.Broadcast()
will wake up p.cond.Wait()
, but the goroutine Signal()/Broadcast()
is located needs to call the unlock method. p.cond.Wait()
the goroutine that calls 060c2a083cbcf5 is awakened, the internal lock operation will be executed again (that is, p.lock.Lock()
is called), so p.cond.Wait()
is still executed in a locked state.
Finally, put the overall flow chart:
Clean up expired goWorker
In the NewPool()
function, a goroutine will be started to periodically clean up the expired goWorker
:
func (p *Pool) purgePeriodically() {
heartbeat := time.NewTicker(p.options.ExpiryDuration)
defer heartbeat.Stop()
for range heartbeat.C {
if p.IsClosed() {
break
}
p.lock.Lock()
expiredWorkers := p.workers.retrieveExpiry(p.options.ExpiryDuration)
p.lock.Unlock()
for i := range expiredWorkers {
expiredWorkers[i].task <- nil
expiredWorkers[i] = nil
}
if p.Running() == 0 {
p.cond.Broadcast()
}
}
}
If the pool is closed, exit the goroutine directly. The cleaning interval is set by the option ExpiryDuration
. If this option is not set, the default value of 1s is used:
// src/github.com/panjf2000/ants/pool.go
func NewPool(size int, options ...Option) (*Pool, error) {
if expiry := opts.ExpiryDuration; expiry < 0 {
return nil, ErrInvalidPoolExpiry
} else if expiry == 0 {
opts.ExpiryDuration = DefaultCleanIntervalTime
}
}
// src/github.com/panjf2000/ants/pool.go
const (
DefaultCleanIntervalTime = time.Second
)
Then every clean-up cycle, call the p.workers.retrieveExpiry()
method to take out the expired goWorker
. Because these goWorker
also blocked in the channel goroutine start task
on, so would like to send the channel a nil
value, goWorker.run()
process receives a value nil
task will return
, goroutine end, to avoid the leakage goroutine .
If all goWorker
have been cleared out, then there might goroutine blocked in retrieveWorker()
method p.cond.Wait()
on, so there need to call p.cond.Broadcast()
wake of these goroutine.
Dynamic capacity modification
During operation, the capacity of the pool can be dynamically modified. Call the p.Tune(size int)
method:
func (p *Pool) Tune(size int) {
if capacity := p.Cap(); capacity == -1 || size <= 0 || size == capacity || p.options.PreAlloc {
return
}
atomic.StoreInt32(&p.capacity, int32(size))
}
Here is just a simple setting of the new capacity, which does not affect the currently executing goWorker
, and if the pre-allocation option is set, the capacity cannot be set again.
The next time revertWorker()
is executed, the new capacity will be used to determine whether it can be goWorker
. The retrieveWorker()
Shut down and restart Pool
After use, you need to close Pool
to avoid goroutine leakage. Release()
method of the pool object to close:
func (p *Pool) Release() {
atomic.StoreInt32(&p.state, CLOSED)
p.lock.Lock()
p.workers.reset()
p.lock.Unlock()
p.cond.Broadcast()
}
Call p.workers.reset()
end loopQueue
or wokerStack
and do some cleanup work. At the same time, to prevent goroutine from blocking on p.cond.Wait()
, execute p.cond.Broadcast()
once.
workerStack
loopQueue
of reset()
, that is, send the nil
to task
channel to end the goroutine, and then reset each field:
// loopQueue 版本
func (wq *loopQueue) reset() {
if wq.isEmpty() {
return
}
Releasing:
if w := wq.detach(); w != nil {
w.task <- nil
goto Releasing
}
wq.items = wq.items[:0]
wq.size = 0
wq.head = 0
wq.tail = 0
}
// stack 版本
func (wq *workerStack) reset() {
for i := 0; i < wq.len(); i++ {
wq.items[i].task <- nil
wq.items[i] = nil
}
wq.items = wq.items[:0]
}
After the pool is closed, you can also call Reboot()
restart:
func (p *Pool) Reboot() {
if atomic.CompareAndSwapInt32(&p.state, CLOSED, OPENED) {
go p.purgePeriodically()
}
}
Since p.purgePeriodically()
p.Release()
, it exits directly. Here you need to re-open a goroutine to clean up regularly.
PoolWithFunc
and WorkWithFunc
In the previous article, we also introduced another way to create Pool
, that is, NewPoolWithFunc()
, and specify a function. When submitting the task later, call p.Invoke()
provide parameters to execute the function. The Pool and Woker structures created in this way are as follows:
type PoolWithFunc struct {
workers []*goWorkerWithFunc
poolFunc func(interface{})
}
type goWorkerWithFunc struct {
pool *PoolWithFunc
args chan interface{}
recycleTime time.Time
}
It is similar to the Pool
and goWorker
introduced earlier, except that PoolWithFunc
saves the passed-in function object and uses an array to save the worker. goWorkerWithFunc
takes interface{}
as the data type of the args
understand, because there are already functions, and you only need to pass in data as a parameter to run:
func (w *goWorkerWithFunc) run() {
go func() {
for args := range w.args {
if args == nil {
return
}
w.pool.poolFunc(args)
if ok := w.pool.revertWorker(w); !ok {
return
}
}
}()
}
Receive function parameters from the channel and execute the function object saved in the pool.
Other details
task
buffer channel
Remember to create p.workerCache
this sync.Pool
object code it:
p.workerCache.New = func() interface{} {
return &goWorker{
pool: p,
task: make(chan func(), workerChanCap),
}
}
When sync.Pool
is no goWorker
object in 060c2a083cbff3, call the New()
method to create one. Note that the task
channel workerChanCap
as the capacity. This variable is defined in the ants.go
file:
var (
// workerChanCap determines whether the channel of a worker should be a buffered channel
// to get the best performance. Inspired by fasthttp at
// https://github.com/valyala/fasthttp/blob/master/workerpool.go#L139
workerChanCap = func() int {
// Use blocking channel if GOMAXPROCS=1.
// This switches context from sender to receiver immediately,
// which results in higher performance (under go1.5 at least).
if runtime.GOMAXPROCS(0) == 1 {
return 0
}
// Use non-blocking workerChan if GOMAXPROCS>1,
// since otherwise the sender might be dragged down if the receiver is CPU-bound.
return 1
}()
)
In order to facilitate comparison, I also put the notes. ants
refers to the implementation of the well-known Web framework fasthttp
When GOMAXPROCS
is 1 (that is, the number of operating system threads is 1), sending to channel task
will suspend sending goroutines and shift the execution flow to receiving goroutines, which can improve reception processing performance. If GOMAXPROCS
greater than 1, ants
uses a buffered channel, in order to prevent receiving goroutines from being CPU-intensive, causing the sending goroutine to be blocked. The following is the relevant code fasthttp
// src/github.com/valyala/fasthttp/workerpool.go
var workerChanCap = func() int {
// Use blocking workerChan if GOMAXPROCS=1.
// This immediately switches Serve to WorkerFunc, which results
// in higher performance (under go1.5 at least).
if runtime.GOMAXPROCS(0) == 1 {
return 0
}
// Use non-blocking workerChan if GOMAXPROCS>1,
// since otherwise the Serve caller (Acceptor) may lag accepting
// new connections if WorkerFunc is CPU-bound.
return 1
}()
Spin lock
ants
uses atomic.CompareAndSwapUint32()
implement a spin lock. Unlike other types of locks, spin locks will not wait immediately after the lock fails, but will continue to try. This can greatly improve performance for applications that can quickly acquire locks, because it can avoid thread switching caused by locking and unlocking:
type spinLock uint32
func (sl *spinLock) Lock() {
backoff := 1
for !atomic.CompareAndSwapUint32((*uint32)(sl), 0, 1) {
for i := 0; i < backoff; i++ {
runtime.Gosched()
}
backoff <<= 1
}
}
func (sl *spinLock) Unlock() {
atomic.StoreUint32((*uint32)(sl), 0)
}
// NewSpinLock instantiates a spin-lock.
func NewSpinLock() sync.Locker {
return new(spinLock)
}
index is used here to , first wait for 1 cycle, and tell the runtime to switch to other goroutines runtime.Gosched()
If the lock still cannot be obtained, wait 2 more cycles. If it still doesn't work, wait 4, 8, 16,... and so on. This can prevent the lock from being unable to be acquired in a short time, resulting in wasted CPU time.
to sum up
ants
short and concise and does not reference any other third-party libraries. Various details of processing and various performance optimization points are worthy of our careful taste. It is strongly recommended that everyone read the source code. Reading excellent source code can greatly improve your coding literacy.
If you find a fun and useful Go language library, welcome to submit an issue on the Go Daily Library GitHub😄
reference
- ants GitHub:github.com/panjf2000/ants
- Go a library GitHub every day: https://github.com/darjun/go-daily-lib
I
My blog: https://darjun.github.io
Welcome to follow my WeChat public account [GoUpUp], learn together and make progress together~
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。