github.com/haraldrudell/parl@v0.4.176/cyclic-awaitable.go (about)

     1  /*
     2  © 2023-present Harald Rudell <haraldrudell@proton.me> (https://haraldrudell.github.io/haraldrudell/)
     3  All rights reserved
     4  */
     5  
     6  package parl
     7  
     8  import "sync/atomic"
     9  
    10  // CyclicAwaitable is an awaitable that can be re-initialized
    11  //   - initialization-free, start in Open state
    12  //   - one-to-many, happens-before
    13  //   - the synchronization mechanic is closing channel, allowing consumers to await
    14  //     multiple events
    15  //   - [CyclicAwaitable.IsClosed] provides thread-safe observability
    16  //   - [CyclicAwaitable.Close] is idempotent, thread-safe and deferrable
    17  //   - Open means event is pending, Close means event has triggered
    18  //   - [CyclicAwaitable.Open] arms the awaitable returning a channel guaranteed to be
    19  //     open at timeof invocation
    20  //   - —
    21  //   - because Awaitable is renewed, access is via atomic Pointer
    22  //   - Pointer to struct allows for atomic update of IsClosed and Open
    23  //
    24  // Usage:
    25  //
    26  //	valueWaiter *CyclicAwaitable
    27  //	…
    28  //	func (v *V) getOrWaitForValue(value T) {
    29  //	  var hasValue bool
    30  //	  // check if value is already present
    31  //	  if value, hasValue = v.hasValueFromThread(); hasValue {
    32  //	    return
    33  //	  }
    34  //	  // arm cyclable
    35  //	  v.valueWaiter.Open()
    36  //	  // collect any value arriving prior to arming cyclable
    37  //	  if value, hasValue = v.hasValueFromThread(); hasValue {
    38  //	    return
    39  //	  }
    40  //	  <- v.valueWaiter.Ch()
    41  //	  value, _ = v.hasValueFromThread()
    42  //	…
    43  //	func (v *V) threadStoresValue(value T) {
    44  //	  v.store(value)
    45  //	  v.valueWaiter.Close()
    46  type CyclicAwaitable struct{ awp atomic.Pointer[Awaitable] }
    47  
    48  // Ch returns an awaitable channel. Thread-safe
    49  func (a *CyclicAwaitable) Ch() (ch AwaitableCh) { return a.aw().Ch() }
    50  
    51  // isClosed inspects whether the awaitable has been triggered
    52  //   - isClosed indicates that the channel is closed
    53  //   - Thread-safe
    54  func (a *CyclicAwaitable) IsClosed() (isClosed bool) { return a.aw().IsClosed() }
    55  
    56  // Close triggers awaitable by closing the channel
    57  //   - upon return, the channel is guaranteed to be closed
    58  //   - eventuallyConsistent [EvCon]: may return before the channel is atcually closed
    59  //     for higher performance
    60  //   - idempotent, deferrable, panic-free, thread-safe
    61  func (a *CyclicAwaitable) Close(eventuallyConsistent ...bool) (didClose bool) { return a.aw().Close() }
    62  
    63  // Open rearms the awaitable for another cycle
    64  //   - ch is guaranteed to have been open at time of invocation.
    65  //     Because each Open may return a different channel,
    66  //     use of the returned ch offers consistent state
    67  //   - didOpen is true if the channel was encountered closed
    68  //   - idempotent, thread-safe, panic-free
    69  func (a *CyclicAwaitable) Open() (didOpen bool, ch AwaitableCh) {
    70  
    71  	// eventually consistency does not work for Open
    72  	//	- Close involves competing threads operating on a unqiue Awaitable
    73  	//		that has only one event
    74  	//	- eventual consistency for Open would require synchronization
    75  	//		of Close and Open invocations
    76  	//	- this would cost 0.4955 ns for every Close and Open
    77  	//	- potential savings is only 1.5 ns for detecting an Open following another Open
    78  
    79  	// openAwaitable as pointer defers allocation
    80  	var openAwaitable *Awaitable
    81  	for {
    82  
    83  		// inspect existing Awaitable
    84  		var awaitable = a.awp.Load()
    85  		if awaitable != nil && !awaitable.IsClosed() {
    86  			// at time of IsClosed, the channel was open
    87  			ch = awaitable.Ch()
    88  			return // was open return
    89  		}
    90  
    91  		// an Awaitable must be created. It was either:
    92  		//	- uninitialized or
    93  		//	- closed
    94  
    95  		// create Awaitable candidate
    96  		if openAwaitable == nil {
    97  			openAwaitable = &Awaitable{}
    98  		}
    99  		if didOpen = a.awp.CompareAndSwap(awaitable, openAwaitable); didOpen {
   100  			// at time of CAS, channel was open
   101  			ch = openAwaitable.Ch()
   102  			return // did open the channel return
   103  		}
   104  	}
   105  }
   106  
   107  // aw returns the active awaitable using atomic mechanic
   108  func (a *CyclicAwaitable) aw() (aw *Awaitable) {
   109  
   110  	// read allocated Awaitable
   111  	if aw = a.awp.Load(); aw != nil {
   112  		return // existing awaitable return
   113  	}
   114  
   115  	// create authoritative Awaitable
   116  	aw = &Awaitable{}
   117  	if a.awp.CompareAndSwap(nil, aw) {
   118  		return // wrote new awaitable return
   119  	}
   120  
   121  	// return other thread’s Awaitable
   122  	return a.awp.Load()
   123  }