aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRunxi Yu <me@runxiyu.org>2025-02-17 21:12:19 +0800
committerRunxi Yu <me@runxiyu.org>2025-02-17 21:12:19 +0800
commit9e6e2f124b4dc744d39f031525b3dbc32eee7314 (patch)
tree5867138ae82f159b8bc9d2ef27432ea923a72436
parentclog: Add README.md: Please don't use this. (diff)
downloadgo-lindenii-common-9e6e2f124b4dc744d39f031525b3dbc32eee7314.tar.gz
go-lindenii-common-9e6e2f124b4dc744d39f031525b3dbc32eee7314.tar.zst
go-lindenii-common-9e6e2f124b4dc744d39f031525b3dbc32eee7314.zip
cmap: Split to Map and ComparableMap
-rw-r--r--cmap/comparable_map.go539
-rw-r--r--cmap/map.go104
-rw-r--r--cmap/map_test.go76
3 files changed, 544 insertions, 175 deletions
diff --git a/cmap/comparable_map.go b/cmap/comparable_map.go
new file mode 100644
index 0000000..e89175c
--- /dev/null
+++ b/cmap/comparable_map.go
@@ -0,0 +1,539 @@
+// Inspired by github.com/SaveTheRbtz/generic-sync-map-go but technically
+// written from scratch with Go 1.23's sync.Map.
+// Copyright 2024 Runxi Yu (porting it to generics)
+// Copyright 2016 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package cmap
+
+import (
+ "sync"
+ "sync/atomic"
+ "unsafe"
+)
+
+// ComparableMap[K comparable, V comparable] is like a Go map[K]V but is safe for concurrent use
+// by multiple goroutines without additional locking or coordination. Loads,
+// stores, and deletes run in amortized constant time.
+//
+// The ComparableMap type is optimized for two common use cases: (1) when the comparableEntry for a given
+// key is only ever written once but read many times, as in caches that only grow,
+// or (2) when multiple goroutines read, write, and overwrite entries for disjoint
+// sets of keys. In these two cases, use of a ComparableMap may significantly reduce lock
+// contention compared to a Go map paired with a separate [Mutex] or [RWMutex].
+//
+// The zero ComparableMap is empty and ready for use. A ComparableMap must not be copied after first use.
+//
+// In the terminology of [the Go memory model], ComparableMap arranges that a write operation
+// “synchronizes before” any read operation that observes the effect of the write, where
+// read and write operations are defined as follows.
+// [ComparableMap.Load], [ComparableMap.LoadAndDelete], [ComparableMap.LoadOrStore], [ComparableMap.Swap], [ComparableMap.CompareAndSwap],
+// and [ComparableMap.CompareAndDelete] are read operations;
+// [ComparableMap.Delete], [ComparableMap.LoadAndDelete], [ComparableMap.Store], and [ComparableMap.Swap] are write operations;
+// [ComparableMap.LoadOrStore] is a write operation when it returns loaded set to false;
+// [ComparableMap.CompareAndSwap] is a write operation when it returns swapped set to true;
+// and [ComparableMap.CompareAndDelete] is a write operation when it returns deleted set to true.
+//
+// [the Go memory model]: https://go.dev/ref/mem
+type ComparableMap[K comparable, V comparable] struct {
+ mu sync.Mutex
+
+ // read contains the portion of the map's contents that are safe for
+ // concurrent access (with or without mu held).
+ //
+ // The read field itself is always safe to load, but must only be stored with
+ // mu held.
+ //
+ // Entries stored in read may be updated concurrently without mu, but updating
+ // a previously-comparableExpunged comparableEntry requires that the comparableEntry be copied to the dirty
+ // map and uncomparableExpunged with mu held.
+ read atomic.Pointer[comparableReadOnly[K, V]]
+
+ // dirty contains the portion of the map's contents that require mu to be
+ // held. To ensure that the dirty map can be promoted to the read map quickly,
+ // it also includes all of the non-comparableExpunged entries in the read map.
+ //
+ // Expunged entries are not stored in the dirty map. An comparableExpunged comparableEntry in the
+ // clean map must be uncomparableExpunged and added to the dirty map before a new value
+ // can be stored to it.
+ //
+ // If the dirty map is nil, the next write to the map will initialize it by
+ // making a shallow copy of the clean map, omitting stale entries.
+ dirty map[K]*comparableEntry[V]
+
+ // misses counts the number of loads since the read map was last updated that
+ // needed to lock mu to determine whether the key was present.
+ //
+ // Once enough misses have occurred to cover the cost of copying the dirty
+ // map, the dirty map will be promoted to the read map (in the unamended
+ // state) and the next store to the map will make a new dirty copy.
+ misses int
+}
+
+// comparableReadOnly is an immutable struct stored atomically in the ComparableMap.read field.
+type comparableReadOnly[K comparable, V comparable] struct {
+ m map[K]*comparableEntry[V]
+ amended bool // true if the dirty map contains some key not in m.
+}
+
+// comparableExpunged is an arbitrary pointer that marks entries which have been deleted
+// from the dirty map.
+var comparableExpunged = unsafe.Pointer(new(any))
+
+// An comparableEntry is a slot in the map corresponding to a particular key.
+type comparableEntry[V comparable] struct {
+ // p points to the value stored for the comparableEntry.
+ //
+ // If p == nil, the comparableEntry has been deleted, and either m.dirty == nil or
+ // m.dirty[key] is e.
+ //
+ // If p == comparableExpunged, the comparableEntry has been deleted, m.dirty != nil, and the comparableEntry
+ // is missing from m.dirty.
+ //
+ // Otherwise, the comparableEntry is valid and recorded in m.read.m[key] and, if m.dirty
+ // != nil, in m.dirty[key].
+ //
+ // An comparableEntry can be deleted by atomic replacement with nil: when m.dirty is
+ // next created, it will atomically replace nil with comparableExpunged and leave
+ // m.dirty[key] unset.
+ //
+ // An comparableEntry's associated value can be updated by atomic replacement, provided
+ // p != comparableExpunged. If p == comparableExpunged, an comparableEntry's associated value can be updated
+ // only after first setting m.dirty[key] = e so that lookups using the dirty
+ // map find the comparableEntry.
+ p unsafe.Pointer
+}
+
+func newComparableEntry[V comparable](i V) *comparableEntry[V] {
+ return &comparableEntry[V]{p: unsafe.Pointer(&i)}
+}
+
+func (m *ComparableMap[K, V]) loadReadOnly() comparableReadOnly[K, V] {
+ if p := m.read.Load(); p != nil {
+ return *p
+ }
+ return comparableReadOnly[K, V]{}
+}
+
+// Load returns the value stored in the map for a key, or nil if no
+// value is present.
+// The ok result indicates whether value was found in the map.
+func (m *ComparableMap[K, V]) Load(key K) (value V, ok bool) {
+ read := m.loadReadOnly()
+ e, ok := read.m[key]
+ if !ok && read.amended {
+ m.mu.Lock()
+ // Avoid reporting a spurious miss if m.dirty got promoted while we were
+ // blocked on m.mu. (If further loads of the same key will not miss, it's
+ // not worth copying the dirty map for this key.)
+ read = m.loadReadOnly()
+ e, ok = read.m[key]
+ if !ok && read.amended {
+ e, ok = m.dirty[key]
+ // Regardless of whether the comparableEntry was present, record a miss: this key
+ // will take the slow path until the dirty map is promoted to the read
+ // map.
+ m.missLocked()
+ }
+ m.mu.Unlock()
+ }
+ if !ok {
+ return *new(V), false
+ }
+ return e.load()
+}
+
+func (e *comparableEntry[V]) load() (value V, ok bool) {
+ p := atomic.LoadPointer(&e.p)
+ if p == nil || p == comparableExpunged {
+ return value, false
+ }
+ return *(*V)(p), true
+}
+
+// Store sets the value for a key.
+func (m *ComparableMap[K, V]) Store(key K, value V) {
+ _, _ = m.Swap(key, value)
+}
+
+// Clear deletes all the entries, resulting in an empty ComparableMap.
+func (m *ComparableMap[K, V]) Clear() {
+ read := m.loadReadOnly()
+ if len(read.m) == 0 && !read.amended {
+ // Avoid allocating a new comparableReadOnly when the map is already clear.
+ return
+ }
+
+ m.mu.Lock()
+ defer m.mu.Unlock()
+
+ read = m.loadReadOnly()
+ if len(read.m) > 0 || read.amended {
+ m.read.Store(&comparableReadOnly[K, V]{})
+ }
+
+ clear(m.dirty)
+ // Don't immediately promote the newly-cleared dirty map on the next operation.
+ m.misses = 0
+}
+
+// tryCompareAndSwap compare the comparableEntry with the given old value and swaps
+// it with a new value if the comparableEntry is equal to the old value, and the comparableEntry
+// has not been comparableExpunged.
+//
+// If the comparableEntry is comparableExpunged, tryCompareAndSwap returns false and leaves
+// the comparableEntry unchanged.
+func (e *comparableEntry[V]) tryCompareAndSwap(old V, new V) bool {
+ p := atomic.LoadPointer(&e.p)
+ if p == nil || p == comparableExpunged || *(*V)(p) != old { // XXX
+ return false
+ }
+
+ // Copy the pointer after the first load to make this method more amenable
+ // to escape analysis: if the comparison fails from the start, we shouldn't
+ // bother heap-allocating a pointer to store.
+ nc := new
+ for {
+ if atomic.CompareAndSwapPointer(&e.p, p, unsafe.Pointer(&nc)) {
+ return true
+ }
+ p = atomic.LoadPointer(&e.p)
+ if p == nil || p == comparableExpunged || *(*V)(p) != old {
+ return false
+ }
+ }
+}
+
+// unexpungeLocked ensures that the comparableEntry is not marked as comparableExpunged.
+//
+// If the comparableEntry was previously comparableExpunged, it must be added to the dirty map
+// before m.mu is unlocked.
+func (e *comparableEntry[V]) unexpungeLocked() (wasExpunged bool) {
+ return atomic.CompareAndSwapPointer(&e.p, comparableExpunged, nil)
+}
+
+// swapLocked unconditionally swaps a value into the comparableEntry.
+//
+// The comparableEntry must be known not to be comparableExpunged.
+func (e *comparableEntry[V]) swapLocked(i *V) *V {
+ return (*V)(atomic.SwapPointer(&e.p, unsafe.Pointer(i)))
+}
+
+// LoadOrStore returns the existing value for the key if present.
+// Otherwise, it stores and returns the given value.
+// The loaded result is true if the value was loaded, false if stored.
+func (m *ComparableMap[K, V]) LoadOrStore(key K, value V) (actual V, loaded bool) {
+ // Avoid locking if it's a clean hit.
+ read := m.loadReadOnly()
+ if e, ok := read.m[key]; ok {
+ actual, loaded, ok := e.tryLoadOrStore(value)
+ if ok {
+ return actual, loaded
+ }
+ }
+
+ m.mu.Lock()
+ read = m.loadReadOnly()
+ if e, ok := read.m[key]; ok {
+ if e.unexpungeLocked() {
+ m.dirty[key] = e
+ }
+ actual, loaded, _ = e.tryLoadOrStore(value)
+ } else if e, ok := m.dirty[key]; ok {
+ actual, loaded, _ = e.tryLoadOrStore(value)
+ m.missLocked()
+ } else {
+ if !read.amended {
+ // We're adding the first new key to the dirty map.
+ // Make sure it is allocated and mark the read-only map as incomplete.
+ m.dirtyLocked()
+ m.read.Store(&comparableReadOnly[K, V]{m: read.m, amended: true})
+ }
+ m.dirty[key] = newComparableEntry(value)
+ actual, loaded = value, false
+ }
+ m.mu.Unlock()
+
+ return actual, loaded
+}
+
+// tryLoadOrStore atomically loads or stores a value if the comparableEntry is not
+// comparableExpunged.
+//
+// If the comparableEntry is comparableExpunged, tryLoadOrStore leaves the comparableEntry unchanged and
+// returns with ok==false.
+func (e *comparableEntry[V]) tryLoadOrStore(i V) (actual V, loaded, ok bool) {
+ p := atomic.LoadPointer(&e.p)
+ if p == comparableExpunged {
+ return actual, false, false
+ }
+ if p != nil {
+ return *(*V)(p), true, true
+ }
+
+ // Copy the pointer after the first load to make this method more amenable
+ // to escape analysis: if we hit the "load" path or the comparableEntry is comparableExpunged, we
+ // shouldn't bother heap-allocating.
+ ic := i
+ for {
+ if atomic.CompareAndSwapPointer(&e.p, nil, unsafe.Pointer(&ic)) {
+ return i, false, true
+ }
+ p = atomic.LoadPointer(&e.p)
+ if p == comparableExpunged {
+ return actual, false, false
+ }
+ if p != nil {
+ return *(*V)(p), true, true
+ }
+ }
+}
+
+// LoadAndDelete deletes the value for a key, returning the previous value if any.
+// The loaded result reports whether the key was present.
+func (m *ComparableMap[K, V]) LoadAndDelete(key K) (value V, loaded bool) {
+ read := m.loadReadOnly()
+ e, ok := read.m[key]
+ if !ok && read.amended {
+ m.mu.Lock()
+ read = m.loadReadOnly()
+ e, ok = read.m[key]
+ if !ok && read.amended {
+ e, ok = m.dirty[key]
+ delete(m.dirty, key)
+ // Regardless of whether the comparableEntry was present, record a miss: this key
+ // will take the slow path until the dirty map is promoted to the read
+ // map.
+ m.missLocked()
+ }
+ m.mu.Unlock()
+ }
+ if ok {
+ return e.delete()
+ }
+ return value, false
+}
+
+// Delete deletes the value for a key.
+func (m *ComparableMap[K, V]) Delete(key K) {
+ m.LoadAndDelete(key)
+}
+
+func (e *comparableEntry[V]) delete() (value V, ok bool) {
+ for {
+ p := atomic.LoadPointer(&e.p)
+ if p == nil || p == comparableExpunged {
+ return value, false
+ }
+ if atomic.CompareAndSwapPointer(&e.p, p, nil) {
+ return *(*V)(p), true
+ }
+ }
+}
+
+// trySwap swaps a value if the comparableEntry has not been comparableExpunged.
+//
+// If the comparableEntry is comparableExpunged, trySwap returns false and leaves the comparableEntry
+// unchanged.
+func (e *comparableEntry[V]) trySwap(i *V) (*V, bool) {
+ for {
+ p := atomic.LoadPointer(&e.p)
+ if p == comparableExpunged {
+ return nil, false
+ }
+ if atomic.CompareAndSwapPointer(&e.p, p, unsafe.Pointer(i)) {
+ return (*V)(p), true
+ }
+ }
+}
+
+// Swap swaps the value for a key and returns the previous value if any.
+// The loaded result reports whether the key was present.
+func (m *ComparableMap[K, V]) Swap(key K, value V) (previous V, loaded bool) {
+ read := m.loadReadOnly()
+ if e, ok := read.m[key]; ok {
+ if v, ok := e.trySwap(&value); ok {
+ if v == nil {
+ return previous, false
+ }
+ return *v, true
+ }
+ }
+
+ m.mu.Lock()
+ read = m.loadReadOnly()
+ if e, ok := read.m[key]; ok {
+ if e.unexpungeLocked() {
+ // The comparableEntry was previously comparableExpunged, which implies that there is a
+ // non-nil dirty map and this comparableEntry is not in it.
+ m.dirty[key] = e
+ }
+ if v := e.swapLocked(&value); v != nil {
+ loaded = true
+ previous = *v
+ }
+ } else if e, ok := m.dirty[key]; ok {
+ if v := e.swapLocked(&value); v != nil {
+ loaded = true
+ previous = *v
+ }
+ } else {
+ if !read.amended {
+ // We're adding the first new key to the dirty map.
+ // Make sure it is allocated and mark the read-only map as incomplete.
+ m.dirtyLocked()
+ m.read.Store(&comparableReadOnly[K, V]{m: read.m, amended: true})
+ }
+ m.dirty[key] = newComparableEntry(value)
+ }
+ m.mu.Unlock()
+ return previous, loaded
+}
+
+// CompareAndSwap swaps the old and new values for key
+// if the value stored in the map is equal to old.
+// The old value must be of a comparable type.
+func (m *ComparableMap[K, V]) CompareAndSwap(key K, old, new V) (swapped bool) {
+ read := m.loadReadOnly()
+ if e, ok := read.m[key]; ok {
+ return e.tryCompareAndSwap(old, new)
+ } else if !read.amended {
+ return false // No existing value for key.
+ }
+
+ m.mu.Lock()
+ defer m.mu.Unlock()
+ read = m.loadReadOnly()
+ swapped = false
+ if e, ok := read.m[key]; ok {
+ swapped = e.tryCompareAndSwap(old, new)
+ } else if e, ok := m.dirty[key]; ok {
+ swapped = e.tryCompareAndSwap(old, new)
+ // We needed to lock mu in order to load the comparableEntry for key,
+ // and the operation didn't change the set of keys in the map
+ // (so it would be made more efficient by promoting the dirty
+ // map to read-only).
+ // Count it as a miss so that we will eventually switch to the
+ // more efficient steady state.
+ m.missLocked()
+ }
+ return swapped
+}
+
+// CompareAndDelete deletes the comparableEntry for key if its value is equal to old.
+// The old value must be of a comparable type.
+//
+// If there is no current value for key in the map, CompareAndDelete
+// returns false (even if the old value is a nil pointer).
+func (m *ComparableMap[K, V]) CompareAndDelete(key K, old V) (deleted bool) {
+ read := m.loadReadOnly()
+ e, ok := read.m[key]
+ if !ok && read.amended {
+ m.mu.Lock()
+ read = m.loadReadOnly()
+ e, ok = read.m[key]
+ if !ok && read.amended {
+ e, ok = m.dirty[key]
+ // Don't delete key from m.dirty: we still need to do the “compare” part
+ // of the operation. The comparableEntry will eventually be comparableExpunged when the
+ // dirty map is promoted to the read map.
+ //
+ // Regardless of whether the comparableEntry was present, record a miss: this key
+ // will take the slow path until the dirty map is promoted to the read
+ // map.
+ m.missLocked()
+ }
+ m.mu.Unlock()
+ }
+ for ok {
+ p := atomic.LoadPointer(&e.p)
+ if p == nil || p == comparableExpunged || *(*V)(p) != old {
+ return false
+ }
+ if atomic.CompareAndSwapPointer(&e.p, p, nil) {
+ return true
+ }
+ }
+ return false
+}
+
+// Range calls f sequentially for each key and value present in the map.
+// If f returns false, range stops the iteration.
+//
+// Range does not necessarily correspond to any consistent snapshot of the ComparableMap's
+// contents: no key will be visited more than once, but if the value for any key
+// is stored or deleted concurrently (including by f), Range may reflect any
+// mapping for that key from any point during the Range call. Range does not
+// block other methods on the receiver; even f itself may call any method on m.
+//
+// Range may be O(N) with the number of elements in the map even if f returns
+// false after a constant number of calls.
+func (m *ComparableMap[K, V]) Range(f func(key K, value V) bool) {
+ // We need to be able to iterate over all of the keys that were already
+ // present at the start of the call to Range.
+ // If read.amended is false, then read.m satisfies that property without
+ // requiring us to hold m.mu for a long time.
+ read := m.loadReadOnly()
+ if read.amended {
+ // m.dirty contains keys not in read.m. Fortunately, Range is already O(N)
+ // (assuming the caller does not break out early), so a call to Range
+ // amortizes an entire copy of the map: we can promote the dirty copy
+ // immediately!
+ m.mu.Lock()
+ read = m.loadReadOnly()
+ if read.amended {
+ read = comparableReadOnly[K, V]{m: m.dirty}
+ copyRead := read
+ m.read.Store(&copyRead)
+ m.dirty = nil
+ m.misses = 0
+ }
+ m.mu.Unlock()
+ }
+
+ for k, e := range read.m {
+ v, ok := e.load()
+ if !ok {
+ continue
+ }
+ if !f(k, v) {
+ break
+ }
+ }
+}
+
+func (m *ComparableMap[K, V]) missLocked() {
+ m.misses++
+ if m.misses < len(m.dirty) {
+ return
+ }
+ m.read.Store(&comparableReadOnly[K, V]{m: m.dirty})
+ m.dirty = nil
+ m.misses = 0
+}
+
+func (m *ComparableMap[K, V]) dirtyLocked() {
+ if m.dirty != nil {
+ return
+ }
+
+ read := m.loadReadOnly()
+ m.dirty = make(map[K]*comparableEntry[V], len(read.m))
+ for k, e := range read.m {
+ if !e.tryExpungeLocked() {
+ m.dirty[k] = e
+ }
+ }
+}
+
+func (e *comparableEntry[V]) tryExpungeLocked() (isExpunged bool) {
+ p := atomic.LoadPointer(&e.p)
+ for p == nil {
+ if atomic.CompareAndSwapPointer(&e.p, nil, comparableExpunged) {
+ return true
+ }
+ p = atomic.LoadPointer(&e.p)
+ }
+ return p == comparableExpunged
+}
diff --git a/cmap/map.go b/cmap/map.go
index bfc0070..7a1fe5b 100644
--- a/cmap/map.go
+++ b/cmap/map.go
@@ -14,7 +14,7 @@ import (
"unsafe"
)
-// Map[K comparable, V comparable] is like a Go map[K]V but is safe for concurrent use
+// Map[K comparable, V any] is like a Go map[K]V but is safe for concurrent use
// by multiple goroutines without additional locking or coordination. Loads,
// stores, and deletes run in amortized constant time.
//
@@ -37,7 +37,7 @@ import (
// and [Map.CompareAndDelete] is a write operation when it returns deleted set to true.
//
// [the Go memory model]: https://go.dev/ref/mem
-type Map[K comparable, V comparable] struct {
+type Map[K comparable, V any] struct {
mu sync.Mutex
// read contains the portion of the map's contents that are safe for
@@ -73,7 +73,7 @@ type Map[K comparable, V comparable] struct {
}
// readOnly is an immutable struct stored atomically in the Map.read field.
-type readOnly[K comparable, V comparable] struct {
+type readOnly[K comparable, V any] struct {
m map[K]*entry[V]
amended bool // true if the dirty map contains some key not in m.
}
@@ -83,7 +83,7 @@ type readOnly[K comparable, V comparable] struct {
var expunged = unsafe.Pointer(new(any))
// An entry is a slot in the map corresponding to a particular key.
-type entry[V comparable] struct {
+type entry[V any] struct {
// p points to the value stored for the entry.
//
// If p == nil, the entry has been deleted, and either m.dirty == nil or
@@ -106,7 +106,7 @@ type entry[V comparable] struct {
p unsafe.Pointer
}
-func newEntry[V comparable](i V) *entry[V] {
+func newEntry[V any](i V) *entry[V] {
return &entry[V]{p: unsafe.Pointer(&i)}
}
@@ -179,33 +179,6 @@ func (m *Map[K, V]) Clear() {
m.misses = 0
}
-// tryCompareAndSwap compare the entry with the given old value and swaps
-// it with a new value if the entry is equal to the old value, and the entry
-// has not been expunged.
-//
-// If the entry is expunged, tryCompareAndSwap returns false and leaves
-// the entry unchanged.
-func (e *entry[V]) tryCompareAndSwap(old V, new V) bool {
- p := atomic.LoadPointer(&e.p)
- if p == nil || p == expunged || *(*V)(p) != old { // XXX
- return false
- }
-
- // Copy the pointer after the first load to make this method more amenable
- // to escape analysis: if the comparison fails from the start, we shouldn't
- // bother heap-allocating a pointer to store.
- nc := new
- for {
- if atomic.CompareAndSwapPointer(&e.p, p, unsafe.Pointer(&nc)) {
- return true
- }
- p = atomic.LoadPointer(&e.p)
- if p == nil || p == expunged || *(*V)(p) != old {
- return false
- }
- }
-}
-
// unexpungeLocked ensures that the entry is not marked as expunged.
//
// If the entry was previously expunged, it must be added to the dirty map
@@ -392,73 +365,6 @@ func (m *Map[K, V]) Swap(key K, value V) (previous V, loaded bool) {
return previous, loaded
}
-// CompareAndSwap swaps the old and new values for key
-// if the value stored in the map is equal to old.
-// The old value must be of a comparable type.
-func (m *Map[K, V]) CompareAndSwap(key K, old, new V) (swapped bool) {
- read := m.loadReadOnly()
- if e, ok := read.m[key]; ok {
- return e.tryCompareAndSwap(old, new)
- } else if !read.amended {
- return false // No existing value for key.
- }
-
- m.mu.Lock()
- defer m.mu.Unlock()
- read = m.loadReadOnly()
- swapped = false
- if e, ok := read.m[key]; ok {
- swapped = e.tryCompareAndSwap(old, new)
- } else if e, ok := m.dirty[key]; ok {
- swapped = e.tryCompareAndSwap(old, new)
- // We needed to lock mu in order to load the entry for key,
- // and the operation didn't change the set of keys in the map
- // (so it would be made more efficient by promoting the dirty
- // map to read-only).
- // Count it as a miss so that we will eventually switch to the
- // more efficient steady state.
- m.missLocked()
- }
- return swapped
-}
-
-// CompareAndDelete deletes the entry for key if its value is equal to old.
-// The old value must be of a comparable type.
-//
-// If there is no current value for key in the map, CompareAndDelete
-// returns false (even if the old value is a nil pointer).
-func (m *Map[K, V]) CompareAndDelete(key K, old V) (deleted bool) {
- read := m.loadReadOnly()
- e, ok := read.m[key]
- if !ok && read.amended {
- m.mu.Lock()
- read = m.loadReadOnly()
- e, ok = read.m[key]
- if !ok && read.amended {
- e, ok = m.dirty[key]
- // Don't delete key from m.dirty: we still need to do the “compare” part
- // of the operation. The entry will eventually be expunged when the
- // dirty map is promoted to the read map.
- //
- // Regardless of whether the entry was present, record a miss: this key
- // will take the slow path until the dirty map is promoted to the read
- // map.
- m.missLocked()
- }
- m.mu.Unlock()
- }
- for ok {
- p := atomic.LoadPointer(&e.p)
- if p == nil || p == expunged || *(*V)(p) != old {
- return false
- }
- if atomic.CompareAndSwapPointer(&e.p, p, nil) {
- return true
- }
- }
- return false
-}
-
// Range calls f sequentially for each key and value present in the map.
// If f returns false, range stops the iteration.
//
diff --git a/cmap/map_test.go b/cmap/map_test.go
deleted file mode 100644
index c407c18..0000000
--- a/cmap/map_test.go
+++ /dev/null
@@ -1,76 +0,0 @@
-// This is directly ported from github.com/SaveTheRbtz/generic-sync-map-go
-// and does not test the new CompareAndSwap and related functions yet.
-// Copyright 2016 The Go Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-package cmap_test
-
-import (
- "math/rand"
- "runtime"
- "sync"
- "testing"
-
- "go.lindenii.runxiyu.org/lindenii-common/cmap"
-)
-
-func TestConcurrentRange(t *testing.T) {
- const mapSize = 1 << 10
-
- m := new(cmap.Map[int64, int64])
- for n := int64(1); n <= mapSize; n++ {
- m.Store(n, int64(n))
- }
-
- done := make(chan struct{})
- var wg sync.WaitGroup
- defer func() {
- close(done)
- wg.Wait()
- }()
- for g := int64(runtime.GOMAXPROCS(0)); g > 0; g-- {
- r := rand.New(rand.NewSource(g))
- wg.Add(1)
- go func(g int64) {
- defer wg.Done()
- for i := int64(0); ; i++ {
- select {
- case <-done:
- return
- default:
- }
- for n := int64(1); n < mapSize; n++ {
- if r.Int63n(mapSize) == 0 {
- m.Store(n, n*i*g)
- } else {
- m.Load(n)
- }
- }
- }
- }(g)
- }
-
- iters := 1 << 10
- if testing.Short() {
- iters = 16
- }
- for n := iters; n > 0; n-- {
- seen := make(map[int64]bool, mapSize)
-
- m.Range(func(k, v int64) bool {
- if v%k != 0 {
- t.Fatalf("while Storing multiples of %v, Range saw value %v", k, v)
- }
- if seen[k] {
- t.Fatalf("Range visited key %v twice", k)
- }
- seen[k] = true
- return true
- })
-
- if len(seen) != mapSize {
- t.Fatalf("Range visited %v elements of %v-element Map", len(seen), mapSize)
- }
- }
-}