github.com/loov/combiner@v0.1.0/parking.go (about)

     1  package combiner
     2  
     3  import (
     4  	"runtime"
     5  	"sync"
     6  )
     7  
     8  // Parking is a bounded non-spinning combiner queue.
     9  //
    10  // This implementation is useful when the batcher work is large
    11  // ore there are many goroutines concurrently calling Do. A good example
    12  // would be a appending to a file.
    13  type Parking struct {
    14  	limit   int64
    15  	batcher Batcher
    16  	_       [5]int64
    17  	head    nodeptr
    18  	_       [7]int64
    19  	lock    sync.Mutex
    20  	cond    sync.Cond
    21  }
    22  
    23  // NewParking creates a Parking combiner queue
    24  func NewParking(batcher Batcher, limit int) *Parking {
    25  	q := &Parking{}
    26  	q.Init(batcher, limit)
    27  	return q
    28  }
    29  
    30  // Init initializes a Parking combiner.
    31  // Note: NewParking does this automatically.
    32  func (q *Parking) Init(batcher Batcher, limit int) {
    33  	if limit < 0 {
    34  		panic("combiner limit must be positive")
    35  	}
    36  
    37  	q.batcher = batcher
    38  	q.limit = int64(limit)
    39  	q.cond.L = &q.lock
    40  }
    41  
    42  // Do passes value to Batcher and waits for completion
    43  //go:nosplit
    44  //go:noinline
    45  func (q *Parking) Do(arg interface{}) {
    46  	var mynode node
    47  	my := &mynode
    48  	my.argument = arg
    49  	defer runtime.KeepAlive(my)
    50  
    51  	var cmp nodeptr
    52  	for {
    53  		cmp = atomicLoadNodeptr(&q.head)
    54  		xchg := locked
    55  		if cmp != 0 {
    56  			xchg = my.ref()
    57  			my.next = cmp
    58  		}
    59  		if atomicCompareAndSwapNodeptr(&q.head, cmp, xchg) {
    60  			break
    61  		}
    62  	}
    63  
    64  	handoff := false
    65  	if cmp != 0 {
    66  		// busy wait
    67  		for i := 0; i < 8; i++ {
    68  			next := atomicLoadNodeptr(&my.next)
    69  			if next == 0 {
    70  				return
    71  			}
    72  			if next&handoffTag != 0 {
    73  				my.next &^= handoffTag
    74  				handoff = true
    75  				goto combining
    76  			}
    77  		}
    78  
    79  		q.lock.Lock()
    80  		for {
    81  			next := atomicLoadNodeptr(&my.next)
    82  			if next == 0 {
    83  				q.lock.Unlock()
    84  				return
    85  			}
    86  			if next&handoffTag != 0 {
    87  				my.next &^= handoffTag
    88  				handoff = true
    89  				q.lock.Unlock()
    90  				goto combining
    91  			}
    92  
    93  			q.cond.Wait()
    94  		}
    95  	}
    96  
    97  combining:
    98  	q.batcher.Start()
    99  	q.batcher.Do(my.argument)
   100  	count := int64(1)
   101  
   102  	if handoff {
   103  		goto combine
   104  	}
   105  
   106  combinecheck:
   107  	for {
   108  		cmp = atomicLoadNodeptr(&q.head)
   109  		var xchg uintptr = 0
   110  		if cmp != locked {
   111  			xchg = locked
   112  		}
   113  
   114  		if atomicCompareAndSwapNodeptr(&q.head, cmp, xchg) {
   115  			break
   116  		}
   117  	}
   118  
   119  	// No more operations to combine, return.
   120  	if cmp == locked {
   121  		q.batcher.Finish()
   122  
   123  		q.lock.Lock()
   124  		q.cond.Broadcast()
   125  		q.lock.Unlock()
   126  		return
   127  	}
   128  
   129  combine:
   130  	// Execute the list of operations.
   131  	for cmp != locked {
   132  		other := nodeptrToNode(cmp)
   133  		if count == q.limit {
   134  			atomicStoreNodeptr(&other.next, other.next|handoffTag)
   135  
   136  			q.batcher.Finish()
   137  
   138  			q.lock.Lock()
   139  			q.cond.Broadcast()
   140  			q.lock.Unlock()
   141  			return
   142  		}
   143  		cmp = other.next
   144  
   145  		q.batcher.Do(other.argument)
   146  		count++
   147  		// Mark completion.
   148  		atomicStoreNodeptr(&other.next, 0)
   149  	}
   150  
   151  	goto combinecheck
   152  }