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  }