Created
August 16, 2024 16:30
-
-
Save jonfriesen/a71fd2e0162b13ffeeba16add34392d6 to your computer and use it in GitHub Desktop.
Concurrent safe map
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package concurrency | |
import ( | |
"sync" | |
) | |
// ConcurrentMap is a generic map that is safe for concurrent use. | |
// It uses sync.RWMutex for managing concurrent access. | |
type ConcurrentMap[K comparable, V any] struct { | |
mu sync.RWMutex | |
m map[K]V | |
} | |
// NewConcurrentMap creates a new ConcurrentMap. | |
func NewConcurrentMap[K comparable, V any]() *ConcurrentMap[K, V] { | |
return &ConcurrentMap[K, V]{ | |
m: make(map[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 (cm *ConcurrentMap[K, V]) Load(key K) (value V, ok bool) { | |
cm.mu.RLock() | |
defer cm.mu.RUnlock() | |
value, ok = cm.m[key] | |
return | |
} | |
// Store sets the value for a key. | |
func (cm *ConcurrentMap[K, V]) Store(key K, value V) { | |
cm.mu.Lock() | |
defer cm.mu.Unlock() | |
cm.m[key] = value | |
} | |
// Delete removes the key from the map. | |
func (cm *ConcurrentMap[K, V]) Delete(key K) { | |
cm.mu.Lock() | |
defer cm.mu.Unlock() | |
delete(cm.m, key) | |
} | |
func (cm *ConcurrentMap[K, V]) Len() int { | |
cm.mu.RLock() | |
defer cm.mu.RUnlock() | |
return len(cm.m) | |
} | |
func (cm *ConcurrentMap[K, V]) Copy() map[K]V { | |
cm.mu.RLock() | |
defer cm.mu.RUnlock() | |
return copyMap(cm.m) | |
} | |
func (cm *ConcurrentMap[K, V]) Reset() { | |
cm.mu.Lock() | |
defer cm.mu.Unlock() | |
cm.m = make(map[K]V) | |
} | |
// Iter iterates over the key-value pairs in the ConcurrentMap, applying the provided function f to each pair. | |
// It uses a two-phase approach to balance concurrency safety and memory efficiency: | |
// | |
// 1. Keys Collection Phase: | |
// - Acquires a read lock on the entire map. | |
// - Creates a slice of keys, pre-allocated to the map's current size to minimize allocations. | |
// - Collects all keys from the map into this slice. | |
// - Releases the read lock. | |
// | |
// 2. Iteration Phase: | |
// - Iterates over the collected keys. | |
// - For each key, acquires a read lock, retrieves the value, and releases the lock. | |
// - Applies the function f to each key-value pair. | |
// | |
// Trade-offs and Design Considerations: | |
// - Concurrency: Allows other goroutines to modify the map between the key collection and iteration phases. | |
// - Consistency: Provides a "snapshot" view of keys at collection time, but values are read in real-time. | |
// - Memory Efficiency: | |
// - Only allocates a single slice for keys, avoiding per-item allocations. | |
// - Trades increased memory usage (O(n) for n keys) for reduced lock contention. | |
// | |
// - Performance: | |
// - Minimizes the duration of holding the read lock on the entire map. | |
// - May miss new keys added after the collection phase or process deleted keys. | |
// | |
// - Flexibility: The provided function f can safely modify the map without causing deadlocks. | |
// | |
// Note: This approach may not be suitable for very large maps due to the upfront memory allocation for keys. | |
// For extremely large maps, consider alternative iteration strategies or paginated approaches. | |
func (cm *ConcurrentMap[K, V]) Iter(f func(key K, value V) bool) { | |
cm.mu.RLock() | |
keys := make([]K, 0, len(cm.m)) | |
for k := range cm.m { | |
keys = append(keys, k) | |
} | |
cm.mu.RUnlock() | |
for _, k := range keys { | |
cm.mu.RLock() | |
v, ok := cm.m[k] | |
cm.mu.RUnlock() | |
if !ok { | |
continue // Key was deleted | |
} | |
if !f(k, v) { | |
break | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment