Documentation
¶
Overview ¶
Package defaultdict implements Python's defaultdict, in a way that's both thread-safe and memory-efficient.
There are two example use cases for it:
1. To implement a row lock, that every row (key) has its own lock.
2. To implement a concurrent counter, that every key has its own atomic int as the counter.
Example ¶
This example demonstrates how to use defaultdict to implement a thread-safe counter.
package main
import (
"fmt"
"sync"
"sync/atomic"
"go.yhsif.com/defaultdict"
)
func main() {
generator := func() *int64 {
// Just create a new *int64 so it can be used as atomic int64.
return new(int64)
}
m := defaultdict.New[string](generator)
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
for j := 0; j < i; j++ {
wg.Add(1)
go func(key string) {
defer wg.Done()
atomic.AddInt64(m.Get(key), 1)
}(fmt.Sprintf("key-%d", i))
}
}
wg.Wait()
m.Range(func(key string, value *int64) bool {
fmt.Printf("Key %v was added %d times\n", key, atomic.LoadInt64(value))
return true
})
}
Output: Key key-1 was added 1 times Key key-2 was added 2 times Key key-3 was added 3 times Key key-4 was added 4 times Key key-5 was added 5 times Key key-6 was added 6 times Key key-7 was added 7 times Key key-8 was added 8 times Key key-9 was added 9 times
Index ¶
Examples ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Generator ¶
type Generator[T Pointer] func() T
Generator defines the function used to generate the default value of the map.
func SharedPoolMapGenerator ¶
func SharedPoolMapGenerator[K comparable, V Pointer](g Generator[V]) Generator[Map[K, V]]
SharedPoolMapGenerator creates a Generator that returns a Map using g as the Generator.
It's different from just:
func generator[K comparable, V defaultdict.Pointer](g defaultdict.Generator[V]) Map[K, V] {
return defaultdict.New[K](g)
}
That the Map returned by the same SharedPoolMapGenerator shares the same underlying pool, so it's more memory efficient when used as the second (or third, etc.) layer of a Map.
This is an example of running the benchmark test with go1.15.6:
$ go test -bench . goos: linux goarch: amd64 pkg: go.yhsif.com/defaultdict cpu: Intel(R) Core(TM) i5-7260U CPU @ 2.20GHz BenchmarkSharedPoolMapGenerator/shared-4 9691 117636 ns/op 1093 B/op 5 allocs/op BenchmarkSharedPoolMapGenerator/naive-4 9882 121344 ns/op 3305 B/op 15 allocs/op PASS ok go.yhsif.com/defaultdict 2.368s
type Map ¶
type Map[K comparable, V Pointer] interface { // Same as in sync.Map, see above for notes about the bool returns. Delete(key K) Load(key K) (V, bool) LoadAndDelete(key K) (V, bool) Range(f func(key K, value V) bool) // Same as Load, just without the bool return. Get(key K) V // Implements iter.Seq2[key, value] All() func(yield func(K, V) bool) }
Map defines a map.
There are a few slight differences in Load and LoadAndDelete comparing to sync.Map:
1. The value type is guaranteed to be the same as the type returned by the Generator used to create this DefaultDict, never nil, even if this is a new key.
2. The bool return being false means that the value is directly from the Generator.
type Pointer ¶
type Pointer = any
Pointer is the value type of a Map.
It's used for documentation purpose only.
In a Map, all values should be pointers, and those pointers should be safe to be mutated concurrently, for the following reasons:
1, A Map is for concurrent mutations (if you only need concurrent read-only access, a builtin map would suffice)
2. There's no Store function provided by Map interface. All mutations are done by Get/Load then mutate the returned value directly.
As an example, you can use *int64 as Pointer, and do mutations via atomic int64 operations. But slices, even though they are pointers, should not be used here directly. You usually need to pair slice with a lock.
Example (Slice) ¶
package main
import (
"fmt"
"sync"
"go.yhsif.com/defaultdict"
)
type MySliceValue struct {
lock sync.Mutex
slice []int
}
func (m *MySliceValue) Append(i int) {
m.lock.Lock()
defer m.lock.Unlock()
m.slice = append(m.slice, i)
}
func (m *MySliceValue) Len() int {
m.lock.Lock()
defer m.lock.Unlock()
return len(m.slice)
}
func MySliceValueGenerator() *MySliceValue {
return new(MySliceValue)
}
func main() {
const key = "foo"
m := defaultdict.New[string](MySliceValueGenerator)
m.Get(key).Append(1)
m.Get(key).Append(2)
fmt.Println(m.Get(key).Len()) // 2
fmt.Println(m.Get("bar").Len()) // 0
}
Output: 2 0