github.com/haraldrudell/parl@v0.4.176/counter/access-manager.go (about)

     1  /*
     2  © 2023–present Harald Rudell <harald.rudell@gmail.com> (https://haraldrudell.github.io/haraldrudell/)
     3  ISC License
     4  */
     5  
     6  package counter
     7  
     8  import (
     9  	"math"
    10  	"sync"
    11  	"sync/atomic"
    12  )
    13  
    14  const (
    15  	lockAccessBit       = uint64(1)
    16  	ticketDelta         = uint64(2)
    17  	negativeTicketDelta = math.MaxUint64 - ticketDelta + 1
    18  	atomicsComplete     = lockAccessBit
    19  )
    20  
    21  // accessManager controls atomic and lock-based access
    22  //   - Lock access: defer a.Lock().Unlock()
    23  //   - atomic or lock access: defer RelinquishAccess(RequestAccess())
    24  type accessManager struct {
    25  	// lock for data access used during lock access periods
    26  	accessLock sync.Mutex
    27  
    28  	// number of requestors requiring lock use, atomic
    29  	lockers uint64
    30  	// lock for changing atomic access status
    31  	//	- ensures sequential by-single-thread transitions of lockAccessBit
    32  	//	- reduces [accessManager.before] contention
    33  	controlLock sync.Mutex
    34  	// atomic access ongoing operations counter, atomic
    35  	//	- lowest bit is lockAccessBit
    36  	//	- lockAccessBit set and cleared behind controlLock
    37  	before uint64
    38  }
    39  
    40  // RequestAccess allows a data provider to make writes to
    41  // shared data without knowing if access is atomic or behind lock
    42  //
    43  // Usage:
    44  //
    45  //	func f() {
    46  //	  defer RelinquishAccess(RequestAccess())
    47  //	  …
    48  func (a *accessManager) RequestAccess() (isLockAccess bool) {
    49  
    50  	// deteremine access method
    51  	for {
    52  		var before = atomic.LoadUint64(&a.before)
    53  		isLockAccess = before&lockAccessBit != 0
    54  		// attempt atomic access
    55  		if !isLockAccess {
    56  			if atomic.CompareAndSwapUint64(&a.before, before, before+ticketDelta) {
    57  				return // atomic access return: isLockAccess: false
    58  			}
    59  			continue // try again
    60  		}
    61  
    62  		// it is lock access
    63  		// acquire access lock
    64  		a.accessLock.Lock()
    65  		return // lock acquired return: isLockAccess true
    66  	}
    67  }
    68  
    69  // RelinquishAccess signals that a provider is done writing to
    70  // shared data
    71  func (a *accessManager) RelinquishAccess(isLockAccess bool) {
    72  	if isLockAccess {
    73  		// end lock access
    74  		a.accessLock.Unlock()
    75  		return
    76  	}
    77  
    78  	// end atomic access
    79  	atomic.AddUint64(&a.before, negativeTicketDelta)
    80  }
    81  
    82  // Lock sets access behind lock, clears out atomic operations and
    83  // acquires the access-lock
    84  //
    85  // Usage:
    86  //
    87  //	func f() {
    88  //	  defer a.Lock().Unlock()
    89  //	  …
    90  func (a *accessManager) Lock() (a2 *accessManager) {
    91  	a2 = a
    92  	var before = a.disableAtomic()
    93  	// ensure atomic operations ceased
    94  	for before != atomicsComplete {
    95  		before = atomic.LoadUint64(&a.before)
    96  	}
    97  	// acquire lock
    98  	a.accessLock.Lock()
    99  	return
   100  }
   101  
   102  // Unlock releases the access-lock
   103  func (a *accessManager) Unlock() {
   104  	a.enableAtomic()
   105  	a.accessLock.Unlock()
   106  }
   107  
   108  // disableAtomic transitions to lock-access
   109  //   - on return lock access is set and atomic operations have ceased
   110  func (a *accessManager) disableAtomic() (before uint64) {
   111  
   112  	// prevent lockRequestBit from being cleared
   113  	//	- a.lockers > 0
   114  	atomic.AddUint64(&a.lockers, 1)
   115  	//	- check current state
   116  	if before = atomic.LoadUint64(&a.before); before&lockAccessBit != 0 {
   117  		return // bit set but atomic operations may be ongoing return
   118  	}
   119  	a.controlLock.Lock()
   120  	defer a.controlLock.Unlock()
   121  
   122  	// ensure lockAccessBit true
   123  	//	- check current state
   124  	if before = atomic.LoadUint64(&a.before); before&lockAccessBit != 0 {
   125  		return // bit set but atomic operations may be ongoing return
   126  	}
   127  
   128  	// set lockAccessBit
   129  	for {
   130  		var oldBefore = atomic.LoadUint64(&a.before)
   131  		before = oldBefore | lockAccessBit
   132  		if atomic.CompareAndSwapUint64(&a.before, oldBefore, before) {
   133  			return // bit set but atomic operations may be ongoing return
   134  		}
   135  	}
   136  }
   137  
   138  // enableAtomic ends a lock-access period which may re-enable atomic access
   139  func (a *accessManager) enableAtomic() {
   140  
   141  	// decrement and check if atomic access should be re-enabled
   142  	if atomic.AddUint64(&a.lockers, ^uint64(0)) > 0 {
   143  		return // do not re-enable atomic access: pending lock-use requestors return
   144  	}
   145  	// check if bit already cleared
   146  	if atomic.LoadUint64(&a.before)&lockAccessBit == 0 {
   147  		return // bit already cleared return
   148  	}
   149  	a.controlLock.Lock()
   150  	defer a.controlLock.Unlock()
   151  
   152  	// clear lock-access bit
   153  	// check if bit already cleared
   154  	if atomic.LoadUint64(&a.before)&lockAccessBit == 0 {
   155  		return // bit already cleared return
   156  	}
   157  	for {
   158  
   159  		// if pending Lock invocation: abort
   160  		if atomic.LoadUint64(&a.lockers) > 0 {
   161  			return // no re-enable: more requestors return
   162  		}
   163  
   164  		// attempt to clear bit
   165  		var before = atomic.LoadUint64(&a.before)
   166  		if atomic.CompareAndSwapUint64(&a.before, before, before&^lockAccessBit) {
   167  			return // bit was cleared return
   168  		}
   169  	}
   170  }