github.com/Aoi-hosizora/ahlib@v1.5.1-0.20230404072829-241b93cf91c7/xgopool/xgopool.go (about) 1 package xgopool 2 3 import ( 4 "context" 5 "log" 6 "sync" 7 "sync/atomic" 8 ) 9 10 // GoPool represents a simple goroutine pool with workers capacity, panic handler, worker pool, task pool and task queue. Please 11 // visit https://github.com/bytedance/gopkg/blob/develop/util/gopool/gopool.go for more details. 12 type GoPool struct { 13 workersCap int32 // atomic 14 panicHandler func(context.Context, interface{}) 15 16 workerPool sync.Pool 17 numWorkers int32 // atomic 18 workerMutex sync.Mutex 19 20 taskPool sync.Pool 21 numTasks int32 // atomic 22 taskMutex sync.Mutex 23 taskHead *task 24 taskTail *task 25 } 26 27 const ( 28 panicNonPositiveCap = "xgopool: non-positive workers capacity" 29 ) 30 31 // New creates an empty GoPool with given workers capacity. 32 func New(cap int32) *GoPool { 33 if cap <= 0 { 34 panic(panicNonPositiveCap) 35 } 36 return &GoPool{ 37 workersCap: cap, 38 panicHandler: func(ctx context.Context, i interface{}) { 39 log.Printf("xgopool warning: Goroutine panicked with `%v`", i) 40 }, 41 workerPool: sync.Pool{New: func() interface{} { return &worker{} }}, // make GoPool must not be copied 42 workerMutex: sync.Mutex{}, 43 taskPool: sync.Pool{New: func() interface{} { return &task{} }}, 44 taskMutex: sync.Mutex{}, 45 } 46 } 47 48 // SetWorkersCap sets workers capacity dynamically. 49 func (g *GoPool) SetWorkersCap(cap int32) { 50 if cap <= 0 { 51 panic(panicNonPositiveCap) 52 } 53 atomic.StoreInt32(&g.workersCap, cap) 54 } 55 56 // SetPanicHandler sets panic handlers for goroutine function invoking. 57 func (g *GoPool) SetPanicHandler(handler func(context.Context, interface{})) { 58 g.panicHandler = handler 59 } 60 61 // WorkersCap returns the current workers capacity. 62 func (g *GoPool) WorkersCap() int32 { 63 return atomic.LoadInt32(&g.workersCap) 64 } 65 66 // NumWorkers returns the current workers count. 67 func (g *GoPool) NumWorkers() int32 { 68 return atomic.LoadInt32(&g.numWorkers) 69 } 70 71 // NumTasks returns the count of current workers waiting. 72 func (g *GoPool) NumTasks() int32 { 73 return atomic.LoadInt32(&g.numTasks) 74 } 75 76 // Go creates a task and waits for a worker to be scheduled, and invokes the task function. 77 func (g *GoPool) Go(f func()) { 78 if f != nil { 79 g.CtxGo(context.Background(), func(context.Context) { 80 f() 81 }) 82 } 83 } 84 85 // CtxGo creates a task and waits for a worker to be scheduled and invoke the task function. Note that function in this method 86 // takes context.Context as parameter. 87 func (g *GoPool) CtxGo(ctx context.Context, f func(context.Context)) { 88 if f != nil { 89 t := g.getTask(ctx, f) 90 g.enqueueTask(t) // numTasks++ 91 if g.NumWorkers() < g.WorkersCap() { 92 w := g.getWorker() // numWorkers++ 93 go w.start() 94 } 95 } 96 } 97 98 // task represents a goroutine task, with context.Context, given function and next pointer for task linked list. 99 type task struct { 100 ctx context.Context 101 f func(context.Context) 102 next *task 103 } 104 105 // getTask returns an empty task structure from task sync.Pool and initializes fields. 106 func (g *GoPool) getTask(ctx context.Context, f func(context.Context)) *task { 107 t := g.taskPool.Get().(*task) 108 t.ctx = ctx 109 t.f = f 110 t.next = nil 111 return t 112 } 113 114 // recycleTask empties given task structure and recycles to task sync.Pool. 115 func (g *GoPool) recycleTask(t *task) { 116 t.ctx = nil 117 t.f = nil 118 t.next = nil 119 g.taskPool.Put(t) 120 } 121 122 // enqueueTask enqueues given task to GoPool's task linked list and updates numTasks. 123 func (g *GoPool) enqueueTask(t *task) { 124 g.taskMutex.Lock() 125 defer g.taskMutex.Unlock() 126 if g.taskHead == nil { 127 g.taskHead = t 128 g.taskTail = t 129 } else { 130 g.taskTail.next = t 131 g.taskTail = t 132 } 133 atomic.AddInt32(&g.numTasks, 1) 134 } 135 136 // dequeueTask dequeues a task from the head of GoPool's task linked list and updates numTasks, returns false if the task list is empty. 137 func (g *GoPool) dequeueTask() (*task, bool) { 138 g.taskMutex.Lock() 139 defer g.taskMutex.Unlock() 140 if g.taskHead == nil { 141 return nil, false 142 } 143 t := g.taskHead 144 g.taskHead = g.taskHead.next 145 atomic.AddInt32(&g.numTasks, -1) 146 return t, true 147 } 148 149 // worker represents a goroutine worker, and is used to execute task. 150 type worker struct { 151 g *GoPool 152 } 153 154 // getWorker returns an empty worker structure from worker sync.Pool and updates numWorkers. 155 func (g *GoPool) getWorker() *worker { 156 g.workerMutex.Lock() 157 defer g.workerMutex.Unlock() 158 w := g.workerPool.Get().(*worker) 159 w.g = g 160 atomic.AddInt32(&g.numWorkers, 1) 161 return w 162 } 163 164 // recycleWorker recycles to worker sync.Pool and updates numWorkers. 165 func (g *GoPool) recycleWorker(w *worker) { 166 g.workerMutex.Lock() 167 defer g.workerMutex.Unlock() 168 w.g = nil 169 g.workerPool.Put(w) 170 atomic.AddInt32(&g.numWorkers, -1) 171 } 172 173 // _testFlag is only used when testing the xgopool package, it represents that now is testing if it equals to true. 174 var _testFlag atomic.Value 175 176 // start dequeues a task from the head of GoPool's task linked list, and invokes given function with panic handler. 177 func (w *worker) start() { 178 defer w.g.recycleWorker(w) // numWorkers-- 179 for { 180 t, ok := w.g.dequeueTask() // numTasks-- 181 if !ok { 182 break 183 } 184 func() { 185 defer func() { 186 if hdl := w.g.panicHandler; hdl != nil { 187 if i := recover(); i != nil { 188 hdl(t.ctx, i) 189 } 190 } else if _testFlag.Load() == true { 191 // enter only when testing xgopool package 192 if i := recover(); i != nil { 193 defer func() { 194 log.Printf("Panic when testing: `%v`", i) 195 _testFlag.Store(false) 196 }() 197 } 198 } 199 }() 200 t.f(t.ctx) 201 }() 202 w.g.recycleTask(t) 203 } 204 } 205 206 // _defaultPool is a global GoPool with capacity 10000. 207 var _defaultPool = New(10000) 208 209 // SetWorkersCap sets workers capacity dynamically. 210 func SetWorkersCap(cap int32) { 211 _defaultPool.SetWorkersCap(cap) 212 } 213 214 // SetPanicHandler sets panic handlers for goroutine function invoking. 215 func SetPanicHandler(handler func(context.Context, interface{})) { 216 _defaultPool.SetPanicHandler(handler) 217 } 218 219 // WorkersCap returns the current workers capacity. 220 func WorkersCap() int32 { 221 return _defaultPool.WorkersCap() 222 } 223 224 // NumWorkers returns the current workers count. 225 func NumWorkers() int32 { 226 return _defaultPool.NumWorkers() 227 } 228 229 // NumTasks returns the count of current workers waiting. 230 func NumTasks() int32 { 231 return _defaultPool.NumTasks() 232 } 233 234 // Go creates a task and waits for a worker to be scheduled, and invokes the task function. 235 func Go(f func()) { 236 _defaultPool.Go(f) 237 } 238 239 // CtxGo creates a task and waits for a worker to be scheduled and invokes the task function. Note that function in this method 240 // takes context.Context as parameter. 241 func CtxGo(ctx context.Context, f func(context.Context)) { 242 _defaultPool.CtxGo(ctx, f) 243 }