Skip to content

Instantly share code, notes, and snippets.

@jonfriesen
Created August 16, 2024 16:30
Show Gist options
  • Save jonfriesen/a71fd2e0162b13ffeeba16add34392d6 to your computer and use it in GitHub Desktop.
Save jonfriesen/a71fd2e0162b13ffeeba16add34392d6 to your computer and use it in GitHub Desktop.
Concurrent safe map
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