github.com/haraldrudell/parl@v0.4.176/wait-group-ch.go (about)

     1  /*
     2  © 2021–present Harald Rudell <harald.rudell@gmail.com> (https://haraldrudell.github.io/haraldrudell/)
     3  ISC License
     4  */
     5  
     6  package parl
     7  
     8  import (
     9  	"fmt"
    10  	"sync"
    11  	"sync/atomic"
    12  
    13  	"github.com/haraldrudell/parl/perrors"
    14  )
    15  
    16  // WaitGroupCh is like a [sync.WaitGroup] with channel-wait mechanic.
    17  // therefore, unlike sync, WaitGroupCh is wait-free and observable.
    18  // WaitGroupCh waits for a collection of goroutines to finish.
    19  // The main goroutine increments the counter for each goroutine.
    20  // Then each of the goroutines decrements the counter until zero.
    21  // Wait or Ch can be used to block until all goroutines have finished.
    22  // A WaitGroup must not be copied after first use.
    23  // In the terminology of the Go memory model, decrementing
    24  // “synchronizes before” the return of any Wait call or Ch read that it unblocks.
    25  //   - counter is increased by [WaitGroupCh.Add] or [WaitGroupCh.Count]
    26  //   - counter is decreased by [WaitGroupCh.Done] [WaitGroupCh.Add]
    27  //     [WaitGroupCh.DoneBool] or [WaitGroupCh.Count]
    28  //   - counter zero is awaited by [WaitGroupCh.Ch] or [WaitGroupCh.Wait]
    29  //   - observability if provided by [WaitGroupCh.Count] [WaitGroupCh.DoneBool] and
    30  //     [WaitGroupCh.IsZero]
    31  //   - panic: negative counter from invoking decreasing methods is panic
    32  //   - panic: adjusting away from zero after invocation of [WaitGroupCh.Ch] or
    33  //     [WaitGroupCh.Wait] is panic.
    34  //     NOTE: all Add should happen prior to invoking Ch or Wait
    35  //   - —
    36  //   - WaitGroupCh is wait-free, observable, initialization-free, thread-safe
    37  //   - channel-wait mechanic allows the consumer to be wait-free
    38  //     Progress by the consumer-thread is not prevented since:
    39  //   - — the channel can be read non-blocking
    40  //   - — consumers can wait for multiple channel events
    41  //   - — consumers are not contending for a lock with any other thread
    42  //   - WaitGroupCh method-set is a superset of [sync.WaitGroup]
    43  //   - there are race conditions:
    44  //   - — writing a zero-counter record with unclosed channel,
    45  //     therefore closing the channel
    46  //   - — reading channel event while a zero-counter record exists,
    47  //     therefore closing the channel
    48  //   - — impact is that a panic might be missed
    49  //
    50  // Usage:
    51  //
    52  //	var w parl.WaitGroupCh
    53  //	w.Add(1)
    54  //	go someFunc(&w)
    55  //	…
    56  //	<-w.Ch()
    57  //	func someFunc(w parl.Doneable) {
    58  //	  defer w.Done()
    59  type WaitGroupCh struct {
    60  	// p as atomic pointer provides integrity in reading counters without a lock
    61  	//	- atomic Pointer enables initialization-free operation
    62  	p atomic.Pointer[addsDones]
    63  }
    64  
    65  // addsDone provides integrity in reading counters without a lock
    66  //   - thread-safe access is provided by reading WaitGroupCh.p
    67  type addsDones struct {
    68  	// pointer to be shared across generations
    69  	ch *Awaitable
    70  	// cumulative positive adds
    71  	adds int
    72  	// cumulative negative adds
    73  	dones int
    74  	// flags that further adjustments are not possible
    75  	channelAboutToClose bool
    76  }
    77  
    78  // Done decrements the WaitGroup counter by one.
    79  func (w *WaitGroupCh) DoneBool() (isExit bool) {
    80  	var count, _ = w.Count(-1)
    81  	return count == 0
    82  }
    83  
    84  // Count returns the current state optionally adjusting the counter
    85  //   - delta is optional counter adjustment
    86  //   - currentCount is current remaining count
    87  //   - totalAdds is cumulative positive adds over WaitGroup lifetime
    88  func (w *WaitGroupCh) Count(delta ...int) (currentCount, totalAdds int) {
    89  	var newAddsDones addsDones
    90  	var channelShouldClose bool
    91  	for {
    92  		var p = w.getP()
    93  
    94  		// delta absent or zero case: return state
    95  		if len(delta) == 0 || delta[0] == 0 {
    96  			currentCount = p.adds - p.dones
    97  			totalAdds = p.adds
    98  			return // no adjustment return
    99  		}
   100  
   101  		// check for channel about to close state
   102  		//	- d is known to be non-zero
   103  		var d = delta[0]
   104  		if p.channelAboutToClose {
   105  			panic(perrors.ErrorfPF("attempt to adjust away from zero when Ch or Wait invoked: delta: %d", d))
   106  		}
   107  
   108  		// create new record
   109  		newAddsDones = *p
   110  		if d > 0 {
   111  			// increment case
   112  			newAddsDones.adds += d
   113  		} else if d < 0 {
   114  			// decrement case
   115  			if p.dones+(-d) > p.adds {
   116  				panic(perrors.ErrorfPF("attempt to adjust to negative: d: %d adds: %d dones: %d",
   117  					d, p.adds, p.dones,
   118  				))
   119  			}
   120  			newAddsDones.dones += (-d)
   121  			// check channel should close
   122  			if newAddsDones.dones == newAddsDones.adds {
   123  				if channelShouldClose = !newAddsDones.channelAboutToClose; channelShouldClose {
   124  					newAddsDones.channelAboutToClose = true
   125  				}
   126  			}
   127  		}
   128  		if w.p.CompareAndSwap(p, &newAddsDones) {
   129  			break // successfully wrote new record
   130  		}
   131  	}
   132  	currentCount = newAddsDones.adds - newAddsDones.dones
   133  	totalAdds = newAddsDones.adds
   134  	if !channelShouldClose {
   135  		return // not closing channel now
   136  	}
   137  
   138  	// trig the awaitable
   139  	newAddsDones.ch.Close()
   140  
   141  	return
   142  }
   143  
   144  // IsZero returns whether the counter is currently zero
   145  func (w *WaitGroupCh) IsZero() (isZero bool) {
   146  	var p = w.getP()
   147  	return p.adds == p.dones
   148  }
   149  
   150  // Add adds delta, which may be negative, to the WaitGroup counter
   151  //   - If the counter becomes zero, all goroutines blocked on Wait are released
   152  //   - If the counter goes negative, Add panics
   153  func (w *WaitGroupCh) Add(delta int) { w.Count(delta) }
   154  
   155  // Done decrements the WaitGroup counter by one.
   156  func (w *WaitGroupCh) Done() { w.Count(-1) }
   157  
   158  // Ch returns a channel that closes once the counter reaches zero
   159  func (w *WaitGroupCh) Ch() (awaitableCh AwaitableCh) { return w.getCh() }
   160  
   161  // Wait blocks until the WaitGroup counter is zero.
   162  func (w *WaitGroupCh) Wait() { <-w.getCh() }
   163  
   164  // Reset triggers the current channel and resets the WaitGroup
   165  func (w *WaitGroupCh) Reset() (w2 *WaitGroupCh) {
   166  	w2 = w
   167  	var p = w.p.Swap(nil)
   168  	if p == nil {
   169  		return // was not initialized
   170  	}
   171  	p.ch.Close()
   172  
   173  	return
   174  }
   175  
   176  // getCh returns the channel and notes the channel was read
   177  func (w *WaitGroupCh) getCh() (awaitableCh AwaitableCh) {
   178  	var p *addsDones
   179  	var newAddsDones addsDones
   180  	var shouldCloseChannel bool
   181  	for {
   182  		p = w.getP()
   183  		if shouldCloseChannel = !p.channelAboutToClose && p.adds == p.dones; !shouldCloseChannel {
   184  			break // no requirement to take channel close action
   185  		}
   186  		newAddsDones = *p
   187  		p.channelAboutToClose = true
   188  		if w.p.CompareAndSwap(p, &newAddsDones) {
   189  			p.ch.Close()
   190  			break // closed the channel
   191  		}
   192  	}
   193  
   194  	return p.ch.Ch()
   195  }
   196  
   197  // get ensures that WaitGroupCh is initialized
   198  func (w *WaitGroupCh) getP() (p *addsDones) {
   199  	var newAddsDones *addsDones
   200  	for {
   201  		if p = w.p.Load(); p != nil {
   202  			return // already initialized return
   203  		} else if newAddsDones == nil {
   204  			newAddsDones = &addsDones{ch: &Awaitable{}}
   205  		}
   206  		if w.p.CompareAndSwap(nil, newAddsDones) {
   207  			p = newAddsDones
   208  			return // initialized with new p
   209  		}
   210  	}
   211  }
   212  
   213  func (w *WaitGroupCh) String() (s string) {
   214  	var p = w.getP()
   215  	return fmt.Sprintf("waitGroupCh_count:%d(adds:%d)", p.adds-p.dones, p.adds)
   216  }
   217  
   218  // func (*sync.WaitGroup).Add(delta int)
   219  // func (*sync.WaitGroup).Done()
   220  // func (*sync.WaitGroup).Wait()
   221  var _ sync.WaitGroup
   222  var _ = (&sync.WaitGroup{}).Add
   223  var _ = (&sync.WaitGroup{}).Wait
   224  var _ = (&sync.WaitGroup{}).Done