github.com/ttpreport/gvisor-ligolo@v0.0.0-20240123134145-a858404967ba/pkg/sync/locking/atomicptrmap_goroutine_unsafe.go (about)

     1  package locking
     2  
     3  import (
     4  	"sync/atomic"
     5  	"unsafe"
     6  
     7  	"github.com/ttpreport/gvisor-ligolo/pkg/gohacks"
     8  	"github.com/ttpreport/gvisor-ligolo/pkg/sync"
     9  )
    10  
    11  const (
    12  	// ShardOrder is an optional parameter specifying the base-2 log of the
    13  	// number of shards per AtomicPtrMap. Higher values of ShardOrder reduce
    14  	// unnecessary synchronization between unrelated concurrent operations,
    15  	// improving performance for write-heavy workloads, but increase memory
    16  	// usage for small maps.
    17  	goroutineLocksShardOrder = 0
    18  )
    19  
    20  // Hasher is an optional type parameter. If Hasher is provided, it must define
    21  // the Init and Hash methods. One Hasher will be shared by all AtomicPtrMaps.
    22  type goroutineLocksHasher struct {
    23  	goroutineLocksdefaultHasher
    24  }
    25  
    26  // defaultHasher is the default Hasher. This indirection exists because
    27  // defaultHasher must exist even if a custom Hasher is provided, to prevent the
    28  // Go compiler from complaining about defaultHasher's unused imports.
    29  type goroutineLocksdefaultHasher struct {
    30  	fn   func(unsafe.Pointer, uintptr) uintptr
    31  	seed uintptr
    32  }
    33  
    34  // Init initializes the Hasher.
    35  func (h *goroutineLocksdefaultHasher) Init() {
    36  	h.fn = sync.MapKeyHasher(map[int64]*goroutineLocks(nil))
    37  	h.seed = sync.RandUintptr()
    38  }
    39  
    40  // Hash returns the hash value for the given Key.
    41  func (h *goroutineLocksdefaultHasher) Hash(key int64) uintptr {
    42  	return h.fn(gohacks.Noescape(unsafe.Pointer(&key)), h.seed)
    43  }
    44  
    45  var goroutineLockshasher goroutineLocksHasher
    46  
    47  func init() {
    48  	goroutineLockshasher.Init()
    49  }
    50  
    51  // An AtomicPtrMap maps Keys to non-nil pointers to Values. AtomicPtrMap are
    52  // safe for concurrent use from multiple goroutines without additional
    53  // synchronization.
    54  //
    55  // The zero value of AtomicPtrMap is empty (maps all Keys to nil) and ready for
    56  // use. AtomicPtrMaps must not be copied after first use.
    57  //
    58  // sync.Map may be faster than AtomicPtrMap if most operations on the map are
    59  // concurrent writes to a fixed set of keys. AtomicPtrMap is usually faster in
    60  // other circumstances.
    61  type goroutineLocksAtomicPtrMap struct {
    62  	shards [1 << goroutineLocksShardOrder]goroutineLocksapmShard
    63  }
    64  
    65  func (m *goroutineLocksAtomicPtrMap) shard(hash uintptr) *goroutineLocksapmShard {
    66  	// Go defines right shifts >= width of shifted unsigned operand as 0, so
    67  	// this is correct even if ShardOrder is 0 (although nogo complains because
    68  	// nogo is dumb).
    69  	const indexLSB = unsafe.Sizeof(uintptr(0))*8 - goroutineLocksShardOrder
    70  	index := hash >> indexLSB
    71  	return (*goroutineLocksapmShard)(unsafe.Pointer(uintptr(unsafe.Pointer(&m.shards)) + (index * unsafe.Sizeof(goroutineLocksapmShard{}))))
    72  }
    73  
    74  type goroutineLocksapmShard struct {
    75  	goroutineLocksapmShardMutationData
    76  	_ [goroutineLocksapmShardMutationDataPadding]byte
    77  	goroutineLocksapmShardLookupData
    78  	_ [goroutineLocksapmShardLookupDataPadding]byte
    79  }
    80  
    81  type goroutineLocksapmShardMutationData struct {
    82  	dirtyMu  sync.Mutex // serializes slot transitions out of empty
    83  	dirty    uintptr    // # slots with val != nil
    84  	count    uintptr    // # slots with val != nil and val != tombstone()
    85  	rehashMu sync.Mutex // serializes rehashing
    86  }
    87  
    88  type goroutineLocksapmShardLookupData struct {
    89  	seq   sync.SeqCount  // allows atomic reads of slots+mask
    90  	slots unsafe.Pointer // [mask+1]slot or nil; protected by rehashMu/seq
    91  	mask  uintptr        // always (a power of 2) - 1; protected by rehashMu/seq
    92  }
    93  
    94  const (
    95  	goroutineLockscacheLineBytes = 64
    96  	// Cache line padding is enabled if sharding is.
    97  	goroutineLocksapmEnablePadding = (goroutineLocksShardOrder + 63) >> 6 // 0 if ShardOrder == 0, 1 otherwise
    98  	// The -1 and +1 below are required to ensure that if unsafe.Sizeof(T) %
    99  	// cacheLineBytes == 0, then padding is 0 (rather than cacheLineBytes).
   100  	goroutineLocksapmShardMutationDataRequiredPadding = goroutineLockscacheLineBytes - (((unsafe.Sizeof(goroutineLocksapmShardMutationData{}) - 1) % goroutineLockscacheLineBytes) + 1)
   101  	goroutineLocksapmShardMutationDataPadding         = goroutineLocksapmEnablePadding * goroutineLocksapmShardMutationDataRequiredPadding
   102  	goroutineLocksapmShardLookupDataRequiredPadding   = goroutineLockscacheLineBytes - (((unsafe.Sizeof(goroutineLocksapmShardLookupData{}) - 1) % goroutineLockscacheLineBytes) + 1)
   103  	goroutineLocksapmShardLookupDataPadding           = goroutineLocksapmEnablePadding * goroutineLocksapmShardLookupDataRequiredPadding
   104  
   105  	// These define fractional thresholds for when apmShard.rehash() is called
   106  	// (i.e. the load factor) and when it rehases to a larger table
   107  	// respectively. They are chosen such that the rehash threshold = the
   108  	// expansion threshold + 1/2, so that when reuse of deleted slots is rare
   109  	// or non-existent, rehashing occurs after the insertion of at least 1/2
   110  	// the table's size in new entries, which is acceptably infrequent.
   111  	goroutineLocksapmRehashThresholdNum    = 2
   112  	goroutineLocksapmRehashThresholdDen    = 3
   113  	goroutineLocksapmExpansionThresholdNum = 1
   114  	goroutineLocksapmExpansionThresholdDen = 6
   115  )
   116  
   117  type goroutineLocksapmSlot struct {
   118  	// slot states are indicated by val:
   119  	//
   120  	//	* Empty: val == nil; key is meaningless. May transition to full or
   121  	//		evacuated with dirtyMu locked.
   122  	//
   123  	//	* Full: val != nil, tombstone(), or evacuated(); key is immutable. val
   124  	//		is the Value mapped to key. May transition to deleted or evacuated.
   125  	//
   126  	//	* Deleted: val == tombstone(); key is still immutable. key is mapped to
   127  	//		no Value. May transition to full or evacuated.
   128  	//
   129  	//	* Evacuated: val == evacuated(); key is immutable. Set by rehashing on
   130  	//		slots that have already been moved, requiring readers to wait for
   131  	//		rehashing to complete and use the new table. Terminal state.
   132  	//
   133  	// Note that once val is non-nil, it cannot become nil again. That is, the
   134  	// transition from empty to non-empty is irreversible for a given slot;
   135  	// the only way to create more empty slots is by rehashing.
   136  	val unsafe.Pointer
   137  	key int64
   138  }
   139  
   140  func goroutineLocksapmSlotAt(slots unsafe.Pointer, pos uintptr) *goroutineLocksapmSlot {
   141  	return (*goroutineLocksapmSlot)(unsafe.Pointer(uintptr(slots) + pos*unsafe.Sizeof(goroutineLocksapmSlot{})))
   142  }
   143  
   144  var goroutineLockstombstoneObj byte
   145  
   146  func goroutineLockstombstone() unsafe.Pointer {
   147  	return unsafe.Pointer(&goroutineLockstombstoneObj)
   148  }
   149  
   150  var goroutineLocksevacuatedObj byte
   151  
   152  func goroutineLocksevacuated() unsafe.Pointer {
   153  	return unsafe.Pointer(&goroutineLocksevacuatedObj)
   154  }
   155  
   156  // Load returns the Value stored in m for key.
   157  func (m *goroutineLocksAtomicPtrMap) Load(key int64) *goroutineLocks {
   158  	hash := goroutineLockshasher.Hash(key)
   159  	shard := m.shard(hash)
   160  
   161  retry:
   162  	epoch := shard.seq.BeginRead()
   163  	slots := atomic.LoadPointer(&shard.slots)
   164  	mask := atomic.LoadUintptr(&shard.mask)
   165  	if !shard.seq.ReadOk(epoch) {
   166  		goto retry
   167  	}
   168  	if slots == nil {
   169  		return nil
   170  	}
   171  
   172  	i := hash & mask
   173  	inc := uintptr(1)
   174  	for {
   175  		slot := goroutineLocksapmSlotAt(slots, i)
   176  		slotVal := atomic.LoadPointer(&slot.val)
   177  		if slotVal == nil {
   178  
   179  			return nil
   180  		}
   181  		if slotVal == goroutineLocksevacuated() {
   182  
   183  			goto retry
   184  		}
   185  		if slot.key == key {
   186  			if slotVal == goroutineLockstombstone() {
   187  				return nil
   188  			}
   189  			return (*goroutineLocks)(slotVal)
   190  		}
   191  		i = (i + inc) & mask
   192  		inc++
   193  	}
   194  }
   195  
   196  // Store stores the Value val for key.
   197  func (m *goroutineLocksAtomicPtrMap) Store(key int64, val *goroutineLocks) {
   198  	m.maybeCompareAndSwap(key, false, nil, val)
   199  }
   200  
   201  // Swap stores the Value val for key and returns the previously-mapped Value.
   202  func (m *goroutineLocksAtomicPtrMap) Swap(key int64, val *goroutineLocks) *goroutineLocks {
   203  	return m.maybeCompareAndSwap(key, false, nil, val)
   204  }
   205  
   206  // CompareAndSwap checks that the Value stored for key is oldVal; if it is, it
   207  // stores the Value newVal for key. CompareAndSwap returns the previous Value
   208  // stored for key, whether or not it stores newVal.
   209  func (m *goroutineLocksAtomicPtrMap) CompareAndSwap(key int64, oldVal, newVal *goroutineLocks) *goroutineLocks {
   210  	return m.maybeCompareAndSwap(key, true, oldVal, newVal)
   211  }
   212  
   213  func (m *goroutineLocksAtomicPtrMap) maybeCompareAndSwap(key int64, compare bool, typedOldVal, typedNewVal *goroutineLocks) *goroutineLocks {
   214  	hash := goroutineLockshasher.Hash(key)
   215  	shard := m.shard(hash)
   216  	oldVal := goroutineLockstombstone()
   217  	if typedOldVal != nil {
   218  		oldVal = unsafe.Pointer(typedOldVal)
   219  	}
   220  	newVal := goroutineLockstombstone()
   221  	if typedNewVal != nil {
   222  		newVal = unsafe.Pointer(typedNewVal)
   223  	}
   224  
   225  retry:
   226  	epoch := shard.seq.BeginRead()
   227  	slots := atomic.LoadPointer(&shard.slots)
   228  	mask := atomic.LoadUintptr(&shard.mask)
   229  	if !shard.seq.ReadOk(epoch) {
   230  		goto retry
   231  	}
   232  	if slots == nil {
   233  		if (compare && oldVal != goroutineLockstombstone()) || newVal == goroutineLockstombstone() {
   234  			return nil
   235  		}
   236  
   237  		shard.rehash(nil)
   238  		goto retry
   239  	}
   240  
   241  	i := hash & mask
   242  	inc := uintptr(1)
   243  	for {
   244  		slot := goroutineLocksapmSlotAt(slots, i)
   245  		slotVal := atomic.LoadPointer(&slot.val)
   246  		if slotVal == nil {
   247  			if (compare && oldVal != goroutineLockstombstone()) || newVal == goroutineLockstombstone() {
   248  				return nil
   249  			}
   250  
   251  			shard.dirtyMu.Lock()
   252  			slotVal = atomic.LoadPointer(&slot.val)
   253  			if slotVal == nil {
   254  
   255  				if dirty, capacity := shard.dirty+1, mask+1; dirty*goroutineLocksapmRehashThresholdDen >= capacity*goroutineLocksapmRehashThresholdNum {
   256  					shard.dirtyMu.Unlock()
   257  					shard.rehash(slots)
   258  					goto retry
   259  				}
   260  				slot.key = key
   261  				atomic.StorePointer(&slot.val, newVal)
   262  				shard.dirty++
   263  				atomic.AddUintptr(&shard.count, 1)
   264  				shard.dirtyMu.Unlock()
   265  				return nil
   266  			}
   267  
   268  			shard.dirtyMu.Unlock()
   269  		}
   270  		if slotVal == goroutineLocksevacuated() {
   271  
   272  			goto retry
   273  		}
   274  		if slot.key == key {
   275  
   276  			for {
   277  				if (compare && oldVal != slotVal) || newVal == slotVal {
   278  					if slotVal == goroutineLockstombstone() {
   279  						return nil
   280  					}
   281  					return (*goroutineLocks)(slotVal)
   282  				}
   283  				if atomic.CompareAndSwapPointer(&slot.val, slotVal, newVal) {
   284  					if slotVal == goroutineLockstombstone() {
   285  						atomic.AddUintptr(&shard.count, 1)
   286  						return nil
   287  					}
   288  					if newVal == goroutineLockstombstone() {
   289  						atomic.AddUintptr(&shard.count, ^uintptr(0))
   290  					}
   291  					return (*goroutineLocks)(slotVal)
   292  				}
   293  				slotVal = atomic.LoadPointer(&slot.val)
   294  				if slotVal == goroutineLocksevacuated() {
   295  					goto retry
   296  				}
   297  			}
   298  		}
   299  
   300  		i = (i + inc) & mask
   301  		inc++
   302  	}
   303  }
   304  
   305  // rehash is marked nosplit to avoid preemption during table copying.
   306  //
   307  //go:nosplit
   308  func (shard *goroutineLocksapmShard) rehash(oldSlots unsafe.Pointer) {
   309  	shard.rehashMu.Lock()
   310  	defer shard.rehashMu.Unlock()
   311  
   312  	if shard.slots != oldSlots {
   313  
   314  		return
   315  	}
   316  
   317  	newSize := uintptr(8)
   318  	if oldSlots != nil {
   319  		oldSize := shard.mask + 1
   320  		newSize = oldSize
   321  		if count := atomic.LoadUintptr(&shard.count) + 1; count*goroutineLocksapmExpansionThresholdDen > oldSize*goroutineLocksapmExpansionThresholdNum {
   322  			newSize *= 2
   323  		}
   324  	}
   325  
   326  	newSlotsSlice := make([]goroutineLocksapmSlot, newSize)
   327  	newSlots := unsafe.Pointer(&newSlotsSlice[0])
   328  	newMask := newSize - 1
   329  
   330  	shard.dirtyMu.Lock()
   331  	shard.seq.BeginWrite()
   332  
   333  	if oldSlots != nil {
   334  		realCount := uintptr(0)
   335  
   336  		oldMask := shard.mask
   337  		for i := uintptr(0); i <= oldMask; i++ {
   338  			oldSlot := goroutineLocksapmSlotAt(oldSlots, i)
   339  			val := atomic.SwapPointer(&oldSlot.val, goroutineLocksevacuated())
   340  			if val == nil || val == goroutineLockstombstone() {
   341  				continue
   342  			}
   343  			hash := goroutineLockshasher.Hash(oldSlot.key)
   344  			j := hash & newMask
   345  			inc := uintptr(1)
   346  			for {
   347  				newSlot := goroutineLocksapmSlotAt(newSlots, j)
   348  				if newSlot.val == nil {
   349  					newSlot.val = val
   350  					newSlot.key = oldSlot.key
   351  					break
   352  				}
   353  				j = (j + inc) & newMask
   354  				inc++
   355  			}
   356  			realCount++
   357  		}
   358  
   359  		shard.dirty = realCount
   360  	}
   361  
   362  	atomic.StorePointer(&shard.slots, newSlots)
   363  	atomic.StoreUintptr(&shard.mask, newMask)
   364  
   365  	shard.seq.EndWrite()
   366  	shard.dirtyMu.Unlock()
   367  }
   368  
   369  // Range invokes f on each Key-Value pair stored in m. If any call to f returns
   370  // false, Range stops iteration and returns.
   371  //
   372  // Range does not necessarily correspond to any consistent snapshot of the
   373  // Map's contents: no Key will be visited more than once, but if the Value for
   374  // any Key is stored or deleted concurrently, Range may reflect any mapping for
   375  // that Key from any point during the Range call.
   376  //
   377  // f must not call other methods on m.
   378  func (m *goroutineLocksAtomicPtrMap) Range(f func(key int64, val *goroutineLocks) bool) {
   379  	for si := 0; si < len(m.shards); si++ {
   380  		shard := &m.shards[si]
   381  		if !shard.doRange(f) {
   382  			return
   383  		}
   384  	}
   385  }
   386  
   387  func (shard *goroutineLocksapmShard) doRange(f func(key int64, val *goroutineLocks) bool) bool {
   388  
   389  	shard.rehashMu.Lock()
   390  	defer shard.rehashMu.Unlock()
   391  	slots := shard.slots
   392  	if slots == nil {
   393  		return true
   394  	}
   395  	mask := shard.mask
   396  	for i := uintptr(0); i <= mask; i++ {
   397  		slot := goroutineLocksapmSlotAt(slots, i)
   398  		slotVal := atomic.LoadPointer(&slot.val)
   399  		if slotVal == nil || slotVal == goroutineLockstombstone() {
   400  			continue
   401  		}
   402  		if !f(slot.key, (*goroutineLocks)(slotVal)) {
   403  			return false
   404  		}
   405  	}
   406  	return true
   407  }
   408  
   409  // RangeRepeatable is like Range, but:
   410  //
   411  //   - RangeRepeatable may visit the same Key multiple times in the presence of
   412  //     concurrent mutators, possibly passing different Values to f in different
   413  //     calls.
   414  //
   415  //   - It is safe for f to call other methods on m.
   416  func (m *goroutineLocksAtomicPtrMap) RangeRepeatable(f func(key int64, val *goroutineLocks) bool) {
   417  	for si := 0; si < len(m.shards); si++ {
   418  		shard := &m.shards[si]
   419  
   420  	retry:
   421  		epoch := shard.seq.BeginRead()
   422  		slots := atomic.LoadPointer(&shard.slots)
   423  		mask := atomic.LoadUintptr(&shard.mask)
   424  		if !shard.seq.ReadOk(epoch) {
   425  			goto retry
   426  		}
   427  		if slots == nil {
   428  			continue
   429  		}
   430  
   431  		for i := uintptr(0); i <= mask; i++ {
   432  			slot := goroutineLocksapmSlotAt(slots, i)
   433  			slotVal := atomic.LoadPointer(&slot.val)
   434  			if slotVal == goroutineLocksevacuated() {
   435  				goto retry
   436  			}
   437  			if slotVal == nil || slotVal == goroutineLockstombstone() {
   438  				continue
   439  			}
   440  			if !f(slot.key, (*goroutineLocks)(slotVal)) {
   441  				return
   442  			}
   443  		}
   444  	}
   445  }