github.com/alphadose/itogami@v0.4.1-0.20221016160904-c25d0a36bfe7/pool_func.go (about)

     1  package itogami
     2  
     3  import (
     4  	"sync"
     5  	"sync/atomic"
     6  	"unsafe"
     7  )
     8  
     9  type (
    10  	// a single slot for a worker in PoolWithFunc
    11  	slotFunc[T any] struct {
    12  		threadPtr unsafe.Pointer
    13  		data      T
    14  	}
    15  
    16  	// PoolWithFunc is used for spawning workers for a single pre-defined function with myriad inputs
    17  	// useful for throughput bound cases
    18  	// has lower memory usage and allocs per op than the default Pool
    19  	//
    20  	//	( type -> func(T) {} ) where T is a generic parameter
    21  	PoolWithFunc[T any] struct {
    22  		currSize uint64
    23  		_p1      [cacheLinePadSize - unsafe.Sizeof(uint64(0))]byte
    24  		maxSize  uint64
    25  		alloc    func() any
    26  		free     func(any)
    27  		task     func(T)
    28  		_p2      [cacheLinePadSize - unsafe.Sizeof(uint64(0)) - 3*unsafe.Sizeof(func() {})]byte
    29  		top      atomic.Pointer[dataItem[T]]
    30  		_p3      [cacheLinePadSize - unsafe.Sizeof(atomic.Pointer[dataItem[T]]{})]byte
    31  	}
    32  )
    33  
    34  // NewPoolWithFunc returns a new PoolWithFunc
    35  func NewPoolWithFunc[T any](size uint64, task func(T)) *PoolWithFunc[T] {
    36  	dataPool := sync.Pool{New: func() any { return new(dataItem[T]) }}
    37  	return &PoolWithFunc[T]{maxSize: size, task: task, alloc: dataPool.Get, free: dataPool.Put}
    38  }
    39  
    40  // Invoke invokes the pre-defined method in PoolWithFunc by assigning the data to an already existing worker
    41  // or spawning a new worker given queue size is in limits
    42  func (self *PoolWithFunc[T]) Invoke(value T) {
    43  	var s *slotFunc[T]
    44  	for {
    45  		if s = self.pop(); s != nil {
    46  			s.data = value
    47  			safe_ready(s.threadPtr)
    48  			return
    49  		} else if atomic.AddUint64(&self.currSize, 1) <= self.maxSize {
    50  			s = &slotFunc[T]{data: value}
    51  			go self.loopQ(s)
    52  			return
    53  		} else {
    54  			atomic.AddUint64(&self.currSize, uint64SubtractionConstant)
    55  			mcall(gosched_m)
    56  		}
    57  	}
    58  }
    59  
    60  // represents the infinite loop for a worker goroutine
    61  func (self *PoolWithFunc[T]) loopQ(d *slotFunc[T]) {
    62  	d.threadPtr = GetG()
    63  	for {
    64  		self.task(d.data)
    65  		self.push(d)
    66  		mcall(fast_park)
    67  	}
    68  }
    69  
    70  // Stack implementation below for storing goroutine references
    71  
    72  // a single node in the stack
    73  type dataItem[T any] struct {
    74  	next  atomic.Pointer[dataItem[T]]
    75  	value *slotFunc[T]
    76  }
    77  
    78  // pop pops value from the top of the stack
    79  func (self *PoolWithFunc[T]) pop() (value *slotFunc[T]) {
    80  	var top, next *dataItem[T]
    81  	for {
    82  		top = self.top.Load()
    83  		if top == nil {
    84  			return
    85  		}
    86  		next = top.next.Load()
    87  		if self.top.CompareAndSwap(top, next) {
    88  			value = top.value
    89  			top.value = nil
    90  			top.next.Store(nil)
    91  			self.free(top)
    92  			return
    93  		}
    94  	}
    95  }
    96  
    97  // push pushes a value on top of the stack
    98  func (self *PoolWithFunc[T]) push(v *slotFunc[T]) {
    99  	var (
   100  		top  *dataItem[T]
   101  		item = self.alloc().(*dataItem[T])
   102  	)
   103  	item.value = v
   104  	for {
   105  		top = self.top.Load()
   106  		item.next.Store(top)
   107  		if self.top.CompareAndSwap(top, item) {
   108  			return
   109  		}
   110  	}
   111  }