├── .gitignore ├── .travis.yml ├── LICENSE ├── README-zh.md ├── README.md ├── concurrent_map.go ├── concurrent_map_bench_test.go ├── concurrent_map_test.go └── go.mod /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # This is a weird way of telling Travis to use the fast container-based test 2 | # runner instead of the slow VM-based runner. 3 | sudo: false 4 | 5 | language: go 6 | 7 | # You don't need to test on very old version of the Go compiler. It's the user's 8 | # responsibility to keep their compilers up to date. 9 | go: 10 | - 1.18 11 | 12 | # Only clone the most recent commit. 13 | git: 14 | depth: 1 15 | 16 | # Skip the install step. Don't `go get` dependencies. Only build with the code 17 | # in vendor/ 18 | install: true 19 | 20 | # Don't email me the results of the test runs. 21 | notifications: 22 | email: false 23 | 24 | before_script: 25 | - go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest 26 | 27 | # script always runs to completion (set +e). If we have linter issues AND a 28 | # failing test, we want to see both. Configure golangci-lint with a 29 | # .golangci.yml file at the top level of your repo. 30 | script: 31 | - golangci-lint run # run a bunch of code checkers/linters in parallel 32 | - go test -v -race ./... # Run all the tests with the race detector enabled 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 streamrail 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README-zh.md: -------------------------------------------------------------------------------- 1 | # concurrent map [![Build Status](https://travis-ci.com/orcaman/concurrent-map.svg?branch=master)](https://travis-ci.com/orcaman/concurrent-map) 2 | 3 | 正如 [这里](http://golang.org/doc/faq#atomic_maps) 和 [这里](http://blog.golang.org/go-maps-in-action)所描述的, Go语言原生的`map`类型并不支持并发读写。`concurrent-map`提供了一种高性能的解决方案:通过对内部`map`进行分片,降低锁粒度,从而达到最少的锁等待时间(锁冲突) 4 | 5 | 在Go 1.9之前,go语言标准库中并没有实现并发`map`。在Go 1.9中,引入了`sync.Map`。新的`sync.Map`与此`concurrent-map`有几个关键区别。标准库中的`sync.Map`是专为`append-only`场景设计的。因此,如果您想将`Map`用于一个类似内存数据库,那么使用我们的版本可能会受益。你可以在golang repo上读到更多,[这里](https://github.com/golang/go/issues/21035) and [这里](https://stackoverflow.com/questions/11063473/map-with-concurrent-access) 6 | ***译注:`sync.Map`在读多写少性能比较好,否则并发性能很差*** 7 | 8 | ## 用法 9 | 10 | 导入包: 11 | 12 | ```go 13 | import ( 14 | "github.com/orcaman/concurrent-map/v2" 15 | ) 16 | 17 | ``` 18 | 19 | ```bash 20 | go get "github.com/orcaman/concurrent-map/v2" 21 | ``` 22 | 23 | 现在包被导入到了`cmap`命名空间下 24 | ***译注:通常包的限定前缀(命名空间)是和目录名一致的,但是这个包有点典型😂,不一致!!!所以用的时候注意*** 25 | 26 | ## 示例 27 | 28 | ```go 29 | 30 | // 创建一个新的 map. 31 | m := cmap.New[string]() 32 | 33 | // 设置变量m一个键为“foo”值为“bar”键值对 34 | m.Set("foo", "bar") 35 | 36 | // 从m中获取指定键值. 37 | bar, ok := m.Get("foo") 38 | 39 | // 删除键为“foo”的项 40 | m.Remove("foo") 41 | 42 | ``` 43 | 44 | 更多使用示例请查看`concurrent_map_test.go`. 45 | 46 | 运行测试: 47 | 48 | ```bash 49 | go test "github.com/orcaman/concurrent-map/v2" 50 | ``` 51 | 52 | ## 贡献说明 53 | 54 | 我们非常欢迎大家的贡献。如欲合并贡献,请遵循以下指引: 55 | - 新建一个issue,并且叙述为什么这么做(解决一个bug,增加一个功能,等等) 56 | - 根据核心团队对上述问题的反馈,提交一个PR,描述变更并链接到该问题。 57 | - 新代码必须具有测试覆盖率。 58 | - 如果代码是关于性能问题的,则必须在流程中包括基准测试(无论是在问题中还是在PR中)。 59 | - 一般来说,我们希望`concurrent-map`尽可能简单,且与原生的`map`有相似的操作。当你新建issue时请注意这一点。 60 | 61 | ## 许可证 62 | MIT (see [LICENSE](https://github.com/orcaman/concurrent-map/blob/master/LICENSE) file) 63 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # concurrent map [![Build Status](https://travis-ci.com/orcaman/concurrent-map.svg?branch=master)](https://travis-ci.com/orcaman/concurrent-map) 2 | 3 | As explained [here](http://golang.org/doc/faq#atomic_maps) and [here](http://blog.golang.org/go-maps-in-action), the `map` type in Go doesn't support concurrent reads and writes. `concurrent-map` provides a high-performance solution to this by sharding the map with minimal time spent waiting for locks. 4 | 5 | Prior to Go 1.9, there was no concurrent map implementation in the stdlib. In Go 1.9, `sync.Map` was introduced. The new `sync.Map` has a few key differences from this map. The stdlib `sync.Map` is designed for append-only scenarios. So if you want to use the map for something more like in-memory db, you might benefit from using our version. You can read more about it in the golang repo, for example [here](https://github.com/golang/go/issues/21035) and [here](https://stackoverflow.com/questions/11063473/map-with-concurrent-access) 6 | 7 | ## usage 8 | 9 | Import the package: 10 | 11 | ```go 12 | import ( 13 | "github.com/orcaman/concurrent-map/v2" 14 | ) 15 | 16 | ``` 17 | 18 | ```bash 19 | go get "github.com/orcaman/concurrent-map/v2" 20 | ``` 21 | 22 | The package is now imported under the "cmap" namespace. 23 | 24 | ## example 25 | 26 | ```go 27 | 28 | // Create a new map. 29 | m := cmap.New[string]() 30 | 31 | // Sets item within map, sets "bar" under key "foo" 32 | m.Set("foo", "bar") 33 | 34 | // Retrieve item from map. 35 | bar, ok := m.Get("foo") 36 | 37 | // Removes item under key "foo" 38 | m.Remove("foo") 39 | 40 | ``` 41 | 42 | For more examples have a look at concurrent_map_test.go. 43 | 44 | Running tests: 45 | 46 | ```bash 47 | go test "github.com/orcaman/concurrent-map/v2" 48 | ``` 49 | 50 | ## guidelines for contributing 51 | 52 | Contributions are highly welcome. In order for a contribution to be merged, please follow these guidelines: 53 | - Open an issue and describe what you are after (fixing a bug, adding an enhancement, etc.). 54 | - According to the core team's feedback on the above mentioned issue, submit a pull request, describing the changes and linking to the issue. 55 | - New code must have test coverage. 56 | - If the code is about performance issues, you must include benchmarks in the process (either in the issue or in the PR). 57 | - In general, we would like to keep `concurrent-map` as simple as possible and as similar to the native `map`. Please keep this in mind when opening issues. 58 | 59 | ## language 60 | - [中文说明](./README-zh.md) 61 | 62 | ## license 63 | MIT (see [LICENSE](https://github.com/orcaman/concurrent-map/blob/master/LICENSE) file) 64 | -------------------------------------------------------------------------------- /concurrent_map.go: -------------------------------------------------------------------------------- 1 | package cmap 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "sync" 7 | ) 8 | 9 | var SHARD_COUNT = 32 10 | 11 | type Stringer interface { 12 | fmt.Stringer 13 | comparable 14 | } 15 | 16 | // A "thread" safe map of type string:Anything. 17 | // To avoid lock bottlenecks this map is dived to several (SHARD_COUNT) map shards. 18 | type ConcurrentMap[K comparable, V any] struct { 19 | shards []*ConcurrentMapShared[K, V] 20 | sharding func(key K) uint32 21 | } 22 | 23 | // A "thread" safe string to anything map. 24 | type ConcurrentMapShared[K comparable, V any] struct { 25 | items map[K]V 26 | sync.RWMutex // Read Write mutex, guards access to internal map. 27 | } 28 | 29 | func create[K comparable, V any](sharding func(key K) uint32) ConcurrentMap[K, V] { 30 | m := ConcurrentMap[K, V]{ 31 | sharding: sharding, 32 | shards: make([]*ConcurrentMapShared[K, V], SHARD_COUNT), 33 | } 34 | for i := 0; i < SHARD_COUNT; i++ { 35 | m.shards[i] = &ConcurrentMapShared[K, V]{items: make(map[K]V)} 36 | } 37 | return m 38 | } 39 | 40 | // Creates a new concurrent map. 41 | func New[V any]() ConcurrentMap[string, V] { 42 | return create[string, V](fnv32) 43 | } 44 | 45 | // Creates a new concurrent map. 46 | func NewStringer[K Stringer, V any]() ConcurrentMap[K, V] { 47 | return create[K, V](strfnv32[K]) 48 | } 49 | 50 | // Creates a new concurrent map. 51 | func NewWithCustomShardingFunction[K comparable, V any](sharding func(key K) uint32) ConcurrentMap[K, V] { 52 | return create[K, V](sharding) 53 | } 54 | 55 | // GetShard returns shard under given key 56 | func (m ConcurrentMap[K, V]) GetShard(key K) *ConcurrentMapShared[K, V] { 57 | return m.shards[uint(m.sharding(key))%uint(SHARD_COUNT)] 58 | } 59 | 60 | func (m ConcurrentMap[K, V]) MSet(data map[K]V) { 61 | for key, value := range data { 62 | shard := m.GetShard(key) 63 | shard.Lock() 64 | shard.items[key] = value 65 | shard.Unlock() 66 | } 67 | } 68 | 69 | // Sets the given value under the specified key. 70 | func (m ConcurrentMap[K, V]) Set(key K, value V) { 71 | // Get map shard. 72 | shard := m.GetShard(key) 73 | shard.Lock() 74 | shard.items[key] = value 75 | shard.Unlock() 76 | } 77 | 78 | // Callback to return new element to be inserted into the map 79 | // It is called while lock is held, therefore it MUST NOT 80 | // try to access other keys in same map, as it can lead to deadlock since 81 | // Go sync.RWLock is not reentrant 82 | type UpsertCb[V any] func(exist bool, valueInMap V, newValue V) V 83 | 84 | // Insert or Update - updates existing element or inserts a new one using UpsertCb 85 | func (m ConcurrentMap[K, V]) Upsert(key K, value V, cb UpsertCb[V]) (res V) { 86 | shard := m.GetShard(key) 87 | shard.Lock() 88 | v, ok := shard.items[key] 89 | res = cb(ok, v, value) 90 | shard.items[key] = res 91 | shard.Unlock() 92 | return res 93 | } 94 | 95 | // Sets the given value under the specified key if no value was associated with it. 96 | func (m ConcurrentMap[K, V]) SetIfAbsent(key K, value V) bool { 97 | // Get map shard. 98 | shard := m.GetShard(key) 99 | shard.Lock() 100 | _, ok := shard.items[key] 101 | if !ok { 102 | shard.items[key] = value 103 | } 104 | shard.Unlock() 105 | return !ok 106 | } 107 | 108 | // Get retrieves an element from map under given key. 109 | func (m ConcurrentMap[K, V]) Get(key K) (V, bool) { 110 | // Get shard 111 | shard := m.GetShard(key) 112 | shard.RLock() 113 | // Get item from shard. 114 | val, ok := shard.items[key] 115 | shard.RUnlock() 116 | return val, ok 117 | } 118 | 119 | // Count returns the number of elements within the map. 120 | func (m ConcurrentMap[K, V]) Count() int { 121 | count := 0 122 | for i := 0; i < SHARD_COUNT; i++ { 123 | shard := m.shards[i] 124 | shard.RLock() 125 | count += len(shard.items) 126 | shard.RUnlock() 127 | } 128 | return count 129 | } 130 | 131 | // Looks up an item under specified key 132 | func (m ConcurrentMap[K, V]) Has(key K) bool { 133 | // Get shard 134 | shard := m.GetShard(key) 135 | shard.RLock() 136 | // See if element is within shard. 137 | _, ok := shard.items[key] 138 | shard.RUnlock() 139 | return ok 140 | } 141 | 142 | // Remove removes an element from the map. 143 | func (m ConcurrentMap[K, V]) Remove(key K) { 144 | // Try to get shard. 145 | shard := m.GetShard(key) 146 | shard.Lock() 147 | delete(shard.items, key) 148 | shard.Unlock() 149 | } 150 | 151 | // RemoveCb is a callback executed in a map.RemoveCb() call, while Lock is held 152 | // If returns true, the element will be removed from the map 153 | type RemoveCb[K any, V any] func(key K, v V, exists bool) bool 154 | 155 | // RemoveCb locks the shard containing the key, retrieves its current value and calls the callback with those params 156 | // If callback returns true and element exists, it will remove it from the map 157 | // Returns the value returned by the callback (even if element was not present in the map) 158 | func (m ConcurrentMap[K, V]) RemoveCb(key K, cb RemoveCb[K, V]) bool { 159 | // Try to get shard. 160 | shard := m.GetShard(key) 161 | shard.Lock() 162 | v, ok := shard.items[key] 163 | remove := cb(key, v, ok) 164 | if remove && ok { 165 | delete(shard.items, key) 166 | } 167 | shard.Unlock() 168 | return remove 169 | } 170 | 171 | // Pop removes an element from the map and returns it 172 | func (m ConcurrentMap[K, V]) Pop(key K) (v V, exists bool) { 173 | // Try to get shard. 174 | shard := m.GetShard(key) 175 | shard.Lock() 176 | v, exists = shard.items[key] 177 | delete(shard.items, key) 178 | shard.Unlock() 179 | return v, exists 180 | } 181 | 182 | // IsEmpty checks if map is empty. 183 | func (m ConcurrentMap[K, V]) IsEmpty() bool { 184 | return m.Count() == 0 185 | } 186 | 187 | // Used by the Iter & IterBuffered functions to wrap two variables together over a channel, 188 | type Tuple[K comparable, V any] struct { 189 | Key K 190 | Val V 191 | } 192 | 193 | // Iter returns an iterator which could be used in a for range loop. 194 | // 195 | // Deprecated: using IterBuffered() will get a better performence 196 | func (m ConcurrentMap[K, V]) Iter() <-chan Tuple[K, V] { 197 | chans := snapshot(m) 198 | ch := make(chan Tuple[K, V]) 199 | go fanIn(chans, ch) 200 | return ch 201 | } 202 | 203 | // IterBuffered returns a buffered iterator which could be used in a for range loop. 204 | func (m ConcurrentMap[K, V]) IterBuffered() <-chan Tuple[K, V] { 205 | chans := snapshot(m) 206 | total := 0 207 | for _, c := range chans { 208 | total += cap(c) 209 | } 210 | ch := make(chan Tuple[K, V], total) 211 | go fanIn(chans, ch) 212 | return ch 213 | } 214 | 215 | // Clear removes all items from map. 216 | func (m ConcurrentMap[K, V]) Clear() { 217 | for item := range m.IterBuffered() { 218 | m.Remove(item.Key) 219 | } 220 | } 221 | 222 | // Returns a array of channels that contains elements in each shard, 223 | // which likely takes a snapshot of `m`. 224 | // It returns once the size of each buffered channel is determined, 225 | // before all the channels are populated using goroutines. 226 | func snapshot[K comparable, V any](m ConcurrentMap[K, V]) (chans []chan Tuple[K, V]) { 227 | //When you access map items before initializing. 228 | if len(m.shards) == 0 { 229 | panic(`cmap.ConcurrentMap is not initialized. Should run New() before usage.`) 230 | } 231 | chans = make([]chan Tuple[K, V], SHARD_COUNT) 232 | wg := sync.WaitGroup{} 233 | wg.Add(SHARD_COUNT) 234 | // Foreach shard. 235 | for index, shard := range m.shards { 236 | go func(index int, shard *ConcurrentMapShared[K, V]) { 237 | // Foreach key, value pair. 238 | shard.RLock() 239 | chans[index] = make(chan Tuple[K, V], len(shard.items)) 240 | wg.Done() 241 | for key, val := range shard.items { 242 | chans[index] <- Tuple[K, V]{key, val} 243 | } 244 | shard.RUnlock() 245 | close(chans[index]) 246 | }(index, shard) 247 | } 248 | wg.Wait() 249 | return chans 250 | } 251 | 252 | // fanIn reads elements from channels `chans` into channel `out` 253 | func fanIn[K comparable, V any](chans []chan Tuple[K, V], out chan Tuple[K, V]) { 254 | wg := sync.WaitGroup{} 255 | wg.Add(len(chans)) 256 | for _, ch := range chans { 257 | go func(ch chan Tuple[K, V]) { 258 | for t := range ch { 259 | out <- t 260 | } 261 | wg.Done() 262 | }(ch) 263 | } 264 | wg.Wait() 265 | close(out) 266 | } 267 | 268 | // Items returns all items as map[string]V 269 | func (m ConcurrentMap[K, V]) Items() map[K]V { 270 | tmp := make(map[K]V) 271 | 272 | // Insert items to temporary map. 273 | for item := range m.IterBuffered() { 274 | tmp[item.Key] = item.Val 275 | } 276 | 277 | return tmp 278 | } 279 | 280 | // Iterator callbacalled for every key,value found in 281 | // maps. RLock is held for all calls for a given shard 282 | // therefore callback sess consistent view of a shard, 283 | // but not across the shards 284 | type IterCb[K comparable, V any] func(key K, v V) 285 | 286 | // Callback based iterator, cheapest way to read 287 | // all elements in a map. 288 | func (m ConcurrentMap[K, V]) IterCb(fn IterCb[K, V]) { 289 | for idx := range m.shards { 290 | shard := (m.shards)[idx] 291 | shard.RLock() 292 | for key, value := range shard.items { 293 | fn(key, value) 294 | } 295 | shard.RUnlock() 296 | } 297 | } 298 | 299 | // Keys returns all keys as []string 300 | func (m ConcurrentMap[K, V]) Keys() []K { 301 | count := m.Count() 302 | ch := make(chan K, count) 303 | go func() { 304 | // Foreach shard. 305 | wg := sync.WaitGroup{} 306 | wg.Add(SHARD_COUNT) 307 | for _, shard := range m.shards { 308 | go func(shard *ConcurrentMapShared[K, V]) { 309 | // Foreach key, value pair. 310 | shard.RLock() 311 | for key := range shard.items { 312 | ch <- key 313 | } 314 | shard.RUnlock() 315 | wg.Done() 316 | }(shard) 317 | } 318 | wg.Wait() 319 | close(ch) 320 | }() 321 | 322 | // Generate keys 323 | keys := make([]K, 0, count) 324 | for k := range ch { 325 | keys = append(keys, k) 326 | } 327 | return keys 328 | } 329 | 330 | // Reviles ConcurrentMap "private" variables to json marshal. 331 | func (m ConcurrentMap[K, V]) MarshalJSON() ([]byte, error) { 332 | // Create a temporary map, which will hold all item spread across shards. 333 | tmp := make(map[K]V) 334 | 335 | // Insert items to temporary map. 336 | for item := range m.IterBuffered() { 337 | tmp[item.Key] = item.Val 338 | } 339 | return json.Marshal(tmp) 340 | } 341 | func strfnv32[K fmt.Stringer](key K) uint32 { 342 | return fnv32(key.String()) 343 | } 344 | 345 | func fnv32(key string) uint32 { 346 | hash := uint32(2166136261) 347 | const prime32 = uint32(16777619) 348 | keyLength := len(key) 349 | for i := 0; i < keyLength; i++ { 350 | hash *= prime32 351 | hash ^= uint32(key[i]) 352 | } 353 | return hash 354 | } 355 | 356 | // Reverse process of Marshal. 357 | func (m *ConcurrentMap[K, V]) UnmarshalJSON(b []byte) (err error) { 358 | tmp := make(map[K]V) 359 | 360 | // Unmarshal into a single map. 361 | if err := json.Unmarshal(b, &tmp); err != nil { 362 | return err 363 | } 364 | 365 | // foreach key,value pair in temporary map insert into our concurrent map. 366 | for key, val := range tmp { 367 | m.Set(key, val) 368 | } 369 | return nil 370 | } 371 | -------------------------------------------------------------------------------- /concurrent_map_bench_test.go: -------------------------------------------------------------------------------- 1 | package cmap 2 | 3 | import ( 4 | "strconv" 5 | "sync" 6 | "testing" 7 | ) 8 | 9 | type Integer int 10 | 11 | func (i Integer) String() string { 12 | return strconv.Itoa(int(i)) 13 | } 14 | 15 | func BenchmarkItems(b *testing.B) { 16 | m := New[Animal]() 17 | 18 | // Insert 100 elements. 19 | for i := 0; i < 10000; i++ { 20 | m.Set(strconv.Itoa(i), Animal{strconv.Itoa(i)}) 21 | } 22 | for i := 0; i < b.N; i++ { 23 | m.Items() 24 | } 25 | } 26 | 27 | func BenchmarkItemsInteger(b *testing.B) { 28 | m := NewStringer[Integer, Animal]() 29 | 30 | // Insert 100 elements. 31 | for i := 0; i < 10000; i++ { 32 | m.Set((Integer)(i), Animal{strconv.Itoa(i)}) 33 | } 34 | for i := 0; i < b.N; i++ { 35 | m.Items() 36 | } 37 | } 38 | func directSharding(key uint32) uint32 { 39 | return key 40 | } 41 | 42 | func BenchmarkItemsInt(b *testing.B) { 43 | m := NewWithCustomShardingFunction[uint32, Animal](directSharding) 44 | 45 | // Insert 100 elements. 46 | for i := 0; i < 10000; i++ { 47 | m.Set((uint32)(i), Animal{strconv.Itoa(i)}) 48 | } 49 | for i := 0; i < b.N; i++ { 50 | m.Items() 51 | } 52 | } 53 | 54 | func BenchmarkMarshalJson(b *testing.B) { 55 | m := New[Animal]() 56 | 57 | // Insert 100 elements. 58 | for i := 0; i < 10000; i++ { 59 | m.Set(strconv.Itoa(i), Animal{strconv.Itoa(i)}) 60 | } 61 | for i := 0; i < b.N; i++ { 62 | _, err := m.MarshalJSON() 63 | if err != nil { 64 | b.FailNow() 65 | } 66 | } 67 | } 68 | 69 | func BenchmarkStrconv(b *testing.B) { 70 | for i := 0; i < b.N; i++ { 71 | strconv.Itoa(i) 72 | } 73 | } 74 | 75 | func BenchmarkSingleInsertAbsent(b *testing.B) { 76 | m := New[string]() 77 | b.ResetTimer() 78 | for i := 0; i < b.N; i++ { 79 | m.Set(strconv.Itoa(i), "value") 80 | } 81 | } 82 | 83 | func BenchmarkSingleInsertAbsentSyncMap(b *testing.B) { 84 | var m sync.Map 85 | b.ResetTimer() 86 | for i := 0; i < b.N; i++ { 87 | m.Store(strconv.Itoa(i), "value") 88 | } 89 | } 90 | 91 | func BenchmarkSingleInsertPresent(b *testing.B) { 92 | m := New[string]() 93 | m.Set("key", "value") 94 | b.ResetTimer() 95 | for i := 0; i < b.N; i++ { 96 | m.Set("key", "value") 97 | } 98 | } 99 | 100 | func BenchmarkSingleInsertPresentSyncMap(b *testing.B) { 101 | var m sync.Map 102 | m.Store("key", "value") 103 | b.ResetTimer() 104 | for i := 0; i < b.N; i++ { 105 | m.Store("key", "value") 106 | } 107 | } 108 | 109 | func benchmarkMultiInsertDifferent(b *testing.B) { 110 | m := New[string]() 111 | finished := make(chan struct{}, b.N) 112 | _, set := GetSet(m, finished) 113 | b.ResetTimer() 114 | for i := 0; i < b.N; i++ { 115 | go set(strconv.Itoa(i), "value") 116 | } 117 | for i := 0; i < b.N; i++ { 118 | <-finished 119 | } 120 | } 121 | 122 | func BenchmarkMultiInsertDifferentSyncMap(b *testing.B) { 123 | var m sync.Map 124 | finished := make(chan struct{}, b.N) 125 | _, set := GetSetSyncMap[string, string](&m, finished) 126 | 127 | b.ResetTimer() 128 | for i := 0; i < b.N; i++ { 129 | go set(strconv.Itoa(i), "value") 130 | } 131 | for i := 0; i < b.N; i++ { 132 | <-finished 133 | } 134 | } 135 | 136 | func BenchmarkMultiInsertDifferent_1_Shard(b *testing.B) { 137 | runWithShards(benchmarkMultiInsertDifferent, b, 1) 138 | } 139 | func BenchmarkMultiInsertDifferent_16_Shard(b *testing.B) { 140 | runWithShards(benchmarkMultiInsertDifferent, b, 16) 141 | } 142 | func BenchmarkMultiInsertDifferent_32_Shard(b *testing.B) { 143 | runWithShards(benchmarkMultiInsertDifferent, b, 32) 144 | } 145 | func BenchmarkMultiInsertDifferent_256_Shard(b *testing.B) { 146 | runWithShards(benchmarkMultiGetSetDifferent, b, 256) 147 | } 148 | 149 | func BenchmarkMultiInsertSame(b *testing.B) { 150 | m := New[string]() 151 | finished := make(chan struct{}, b.N) 152 | _, set := GetSet(m, finished) 153 | m.Set("key", "value") 154 | b.ResetTimer() 155 | for i := 0; i < b.N; i++ { 156 | go set("key", "value") 157 | } 158 | for i := 0; i < b.N; i++ { 159 | <-finished 160 | } 161 | } 162 | 163 | func BenchmarkMultiInsertSameSyncMap(b *testing.B) { 164 | var m sync.Map 165 | finished := make(chan struct{}, b.N) 166 | _, set := GetSetSyncMap[string, string](&m, finished) 167 | m.Store("key", "value") 168 | b.ResetTimer() 169 | for i := 0; i < b.N; i++ { 170 | go set("key", "value") 171 | } 172 | for i := 0; i < b.N; i++ { 173 | <-finished 174 | } 175 | } 176 | 177 | func BenchmarkMultiGetSame(b *testing.B) { 178 | m := New[string]() 179 | finished := make(chan struct{}, b.N) 180 | get, _ := GetSet(m, finished) 181 | m.Set("key", "value") 182 | b.ResetTimer() 183 | for i := 0; i < b.N; i++ { 184 | go get("key", "value") 185 | } 186 | for i := 0; i < b.N; i++ { 187 | <-finished 188 | } 189 | } 190 | 191 | func BenchmarkMultiGetSameSyncMap(b *testing.B) { 192 | var m sync.Map 193 | finished := make(chan struct{}, b.N) 194 | get, _ := GetSetSyncMap[string, string](&m, finished) 195 | m.Store("key", "value") 196 | b.ResetTimer() 197 | for i := 0; i < b.N; i++ { 198 | go get("key", "value") 199 | } 200 | for i := 0; i < b.N; i++ { 201 | <-finished 202 | } 203 | } 204 | 205 | func benchmarkMultiGetSetDifferent(b *testing.B) { 206 | m := New[string]() 207 | finished := make(chan struct{}, 2*b.N) 208 | get, set := GetSet(m, finished) 209 | m.Set("-1", "value") 210 | b.ResetTimer() 211 | for i := 0; i < b.N; i++ { 212 | go set(strconv.Itoa(i-1), "value") 213 | go get(strconv.Itoa(i), "value") 214 | } 215 | for i := 0; i < 2*b.N; i++ { 216 | <-finished 217 | } 218 | } 219 | 220 | func BenchmarkMultiGetSetDifferentSyncMap(b *testing.B) { 221 | var m sync.Map 222 | finished := make(chan struct{}, 2*b.N) 223 | get, set := GetSetSyncMap[string, string](&m, finished) 224 | m.Store("-1", "value") 225 | b.ResetTimer() 226 | for i := 0; i < b.N; i++ { 227 | go set(strconv.Itoa(i-1), "value") 228 | go get(strconv.Itoa(i), "value") 229 | } 230 | for i := 0; i < 2*b.N; i++ { 231 | <-finished 232 | } 233 | } 234 | 235 | func BenchmarkMultiGetSetDifferent_1_Shard(b *testing.B) { 236 | runWithShards(benchmarkMultiGetSetDifferent, b, 1) 237 | } 238 | func BenchmarkMultiGetSetDifferent_16_Shard(b *testing.B) { 239 | runWithShards(benchmarkMultiGetSetDifferent, b, 16) 240 | } 241 | func BenchmarkMultiGetSetDifferent_32_Shard(b *testing.B) { 242 | runWithShards(benchmarkMultiGetSetDifferent, b, 32) 243 | } 244 | func BenchmarkMultiGetSetDifferent_256_Shard(b *testing.B) { 245 | runWithShards(benchmarkMultiGetSetDifferent, b, 256) 246 | } 247 | 248 | func benchmarkMultiGetSetBlock(b *testing.B) { 249 | m := New[string]() 250 | finished := make(chan struct{}, 2*b.N) 251 | get, set := GetSet(m, finished) 252 | for i := 0; i < b.N; i++ { 253 | m.Set(strconv.Itoa(i%100), "value") 254 | } 255 | b.ResetTimer() 256 | for i := 0; i < b.N; i++ { 257 | go set(strconv.Itoa(i%100), "value") 258 | go get(strconv.Itoa(i%100), "value") 259 | } 260 | for i := 0; i < 2*b.N; i++ { 261 | <-finished 262 | } 263 | } 264 | 265 | func BenchmarkMultiGetSetBlockSyncMap(b *testing.B) { 266 | var m sync.Map 267 | finished := make(chan struct{}, 2*b.N) 268 | get, set := GetSetSyncMap[string, string](&m, finished) 269 | for i := 0; i < b.N; i++ { 270 | m.Store(strconv.Itoa(i%100), "value") 271 | } 272 | b.ResetTimer() 273 | for i := 0; i < b.N; i++ { 274 | go set(strconv.Itoa(i%100), "value") 275 | go get(strconv.Itoa(i%100), "value") 276 | } 277 | for i := 0; i < 2*b.N; i++ { 278 | <-finished 279 | } 280 | } 281 | 282 | func BenchmarkMultiGetSetBlock_1_Shard(b *testing.B) { 283 | runWithShards(benchmarkMultiGetSetBlock, b, 1) 284 | } 285 | func BenchmarkMultiGetSetBlock_16_Shard(b *testing.B) { 286 | runWithShards(benchmarkMultiGetSetBlock, b, 16) 287 | } 288 | func BenchmarkMultiGetSetBlock_32_Shard(b *testing.B) { 289 | runWithShards(benchmarkMultiGetSetBlock, b, 32) 290 | } 291 | func BenchmarkMultiGetSetBlock_256_Shard(b *testing.B) { 292 | runWithShards(benchmarkMultiGetSetBlock, b, 256) 293 | } 294 | 295 | 296 | func GetSet[K comparable, V any](m ConcurrentMap[K, V], finished chan struct{}) (set func(key K, value V), get func(key K, value V)) { 297 | return func(key K, value V) { 298 | for i := 0; i < 10; i++ { 299 | m.Get(key) 300 | } 301 | finished <- struct{}{} 302 | }, func(key K, value V) { 303 | for i := 0; i < 10; i++ { 304 | m.Set(key, value) 305 | } 306 | finished <- struct{}{} 307 | } 308 | } 309 | 310 | func GetSetSyncMap[K comparable, V any](m *sync.Map, finished chan struct{}) (get func(key K, value V), set func(key K, value V)) { 311 | get = func(key K, value V) { 312 | for i := 0; i < 10; i++ { 313 | m.Load(key) 314 | } 315 | finished <- struct{}{} 316 | } 317 | set = func(key K, value V) { 318 | for i := 0; i < 10; i++ { 319 | m.Store(key, value) 320 | } 321 | finished <- struct{}{} 322 | } 323 | return 324 | } 325 | 326 | func runWithShards(bench func(b *testing.B), b *testing.B, shardsCount int) { 327 | oldShardsCount := SHARD_COUNT 328 | SHARD_COUNT = shardsCount 329 | bench(b) 330 | SHARD_COUNT = oldShardsCount 331 | } 332 | 333 | func BenchmarkKeys(b *testing.B) { 334 | m := New[Animal]() 335 | 336 | // Insert 100 elements. 337 | for i := 0; i < 10000; i++ { 338 | m.Set(strconv.Itoa(i), Animal{strconv.Itoa(i)}) 339 | } 340 | for i := 0; i < b.N; i++ { 341 | m.Keys() 342 | } 343 | } 344 | -------------------------------------------------------------------------------- /concurrent_map_test.go: -------------------------------------------------------------------------------- 1 | package cmap 2 | 3 | import ( 4 | "encoding/json" 5 | "hash/fnv" 6 | "sort" 7 | "strconv" 8 | "testing" 9 | ) 10 | 11 | type Animal struct { 12 | name string 13 | } 14 | 15 | func TestMapCreation(t *testing.T) { 16 | m := New[string]() 17 | if m.shards == nil { 18 | t.Error("map is null.") 19 | } 20 | 21 | if m.Count() != 0 { 22 | t.Error("new map should be empty.") 23 | } 24 | } 25 | 26 | func TestInsert(t *testing.T) { 27 | m := New[Animal]() 28 | elephant := Animal{"elephant"} 29 | monkey := Animal{"monkey"} 30 | 31 | m.Set("elephant", elephant) 32 | m.Set("monkey", monkey) 33 | 34 | if m.Count() != 2 { 35 | t.Error("map should contain exactly two elements.") 36 | } 37 | } 38 | 39 | func TestInsertAbsent(t *testing.T) { 40 | m := New[Animal]() 41 | elephant := Animal{"elephant"} 42 | monkey := Animal{"monkey"} 43 | 44 | m.SetIfAbsent("elephant", elephant) 45 | if ok := m.SetIfAbsent("elephant", monkey); ok { 46 | t.Error("map set a new value even the entry is already present") 47 | } 48 | } 49 | 50 | func TestGet(t *testing.T) { 51 | m := New[Animal]() 52 | 53 | // Get a missing element. 54 | val, ok := m.Get("Money") 55 | 56 | if ok == true { 57 | t.Error("ok should be false when item is missing from map.") 58 | } 59 | 60 | if (val != Animal{}) { 61 | t.Error("Missing values should return as null.") 62 | } 63 | 64 | elephant := Animal{"elephant"} 65 | m.Set("elephant", elephant) 66 | 67 | // Retrieve inserted element. 68 | elephant, ok = m.Get("elephant") 69 | if ok == false { 70 | t.Error("ok should be true for item stored within the map.") 71 | } 72 | 73 | if elephant.name != "elephant" { 74 | t.Error("item was modified.") 75 | } 76 | } 77 | 78 | func TestHas(t *testing.T) { 79 | m := New[Animal]() 80 | 81 | // Get a missing element. 82 | if m.Has("Money") == true { 83 | t.Error("element shouldn't exists") 84 | } 85 | 86 | elephant := Animal{"elephant"} 87 | m.Set("elephant", elephant) 88 | 89 | if m.Has("elephant") == false { 90 | t.Error("element exists, expecting Has to return True.") 91 | } 92 | } 93 | 94 | func TestRemove(t *testing.T) { 95 | m := New[Animal]() 96 | 97 | monkey := Animal{"monkey"} 98 | m.Set("monkey", monkey) 99 | 100 | m.Remove("monkey") 101 | 102 | if m.Count() != 0 { 103 | t.Error("Expecting count to be zero once item was removed.") 104 | } 105 | 106 | temp, ok := m.Get("monkey") 107 | 108 | if ok != false { 109 | t.Error("Expecting ok to be false for missing items.") 110 | } 111 | 112 | if (temp != Animal{}) { 113 | t.Error("Expecting item to be nil after its removal.") 114 | } 115 | 116 | // Remove a none existing element. 117 | m.Remove("noone") 118 | } 119 | 120 | func TestRemoveCb(t *testing.T) { 121 | m := New[Animal]() 122 | 123 | monkey := Animal{"monkey"} 124 | m.Set("monkey", monkey) 125 | elephant := Animal{"elephant"} 126 | m.Set("elephant", elephant) 127 | 128 | var ( 129 | mapKey string 130 | mapVal Animal 131 | wasFound bool 132 | ) 133 | cb := func(key string, val Animal, exists bool) bool { 134 | mapKey = key 135 | mapVal = val 136 | wasFound = exists 137 | 138 | return val.name == "monkey" 139 | } 140 | 141 | // Monkey should be removed 142 | result := m.RemoveCb("monkey", cb) 143 | if !result { 144 | t.Errorf("Result was not true") 145 | } 146 | 147 | if mapKey != "monkey" { 148 | t.Error("Wrong key was provided to the callback") 149 | } 150 | 151 | if mapVal != monkey { 152 | t.Errorf("Wrong value was provided to the value") 153 | } 154 | 155 | if !wasFound { 156 | t.Errorf("Key was not found") 157 | } 158 | 159 | if m.Has("monkey") { 160 | t.Errorf("Key was not removed") 161 | } 162 | 163 | // Elephant should not be removed 164 | result = m.RemoveCb("elephant", cb) 165 | if result { 166 | t.Errorf("Result was true") 167 | } 168 | 169 | if mapKey != "elephant" { 170 | t.Error("Wrong key was provided to the callback") 171 | } 172 | 173 | if mapVal != elephant { 174 | t.Errorf("Wrong value was provided to the value") 175 | } 176 | 177 | if !wasFound { 178 | t.Errorf("Key was not found") 179 | } 180 | 181 | if !m.Has("elephant") { 182 | t.Errorf("Key was removed") 183 | } 184 | 185 | // Unset key should remain unset 186 | result = m.RemoveCb("horse", cb) 187 | if result { 188 | t.Errorf("Result was true") 189 | } 190 | 191 | if mapKey != "horse" { 192 | t.Error("Wrong key was provided to the callback") 193 | } 194 | 195 | if (mapVal != Animal{}) { 196 | t.Errorf("Wrong value was provided to the value") 197 | } 198 | 199 | if wasFound { 200 | t.Errorf("Key was found") 201 | } 202 | 203 | if m.Has("horse") { 204 | t.Errorf("Key was created") 205 | } 206 | } 207 | 208 | func TestPop(t *testing.T) { 209 | m := New[Animal]() 210 | 211 | monkey := Animal{"monkey"} 212 | m.Set("monkey", monkey) 213 | 214 | v, exists := m.Pop("monkey") 215 | 216 | if !exists || v != monkey { 217 | t.Error("Pop didn't find a monkey.") 218 | } 219 | 220 | v2, exists2 := m.Pop("monkey") 221 | 222 | if exists2 || v2 == monkey { 223 | t.Error("Pop keeps finding monkey") 224 | } 225 | 226 | if m.Count() != 0 { 227 | t.Error("Expecting count to be zero once item was Pop'ed.") 228 | } 229 | 230 | temp, ok := m.Get("monkey") 231 | 232 | if ok != false { 233 | t.Error("Expecting ok to be false for missing items.") 234 | } 235 | 236 | if (temp != Animal{}) { 237 | t.Error("Expecting item to be nil after its removal.") 238 | } 239 | } 240 | 241 | func TestCount(t *testing.T) { 242 | m := New[Animal]() 243 | for i := 0; i < 100; i++ { 244 | m.Set(strconv.Itoa(i), Animal{strconv.Itoa(i)}) 245 | } 246 | 247 | if m.Count() != 100 { 248 | t.Error("Expecting 100 element within map.") 249 | } 250 | } 251 | 252 | func TestIsEmpty(t *testing.T) { 253 | m := New[Animal]() 254 | 255 | if m.IsEmpty() == false { 256 | t.Error("new map should be empty") 257 | } 258 | 259 | m.Set("elephant", Animal{"elephant"}) 260 | 261 | if m.IsEmpty() != false { 262 | t.Error("map shouldn't be empty.") 263 | } 264 | } 265 | 266 | func TestIterator(t *testing.T) { 267 | m := New[Animal]() 268 | 269 | // Insert 100 elements. 270 | for i := 0; i < 100; i++ { 271 | m.Set(strconv.Itoa(i), Animal{strconv.Itoa(i)}) 272 | } 273 | 274 | counter := 0 275 | // Iterate over elements. 276 | for item := range m.Iter() { 277 | val := item.Val 278 | 279 | if (val == Animal{}) { 280 | t.Error("Expecting an object.") 281 | } 282 | counter++ 283 | } 284 | 285 | if counter != 100 { 286 | t.Error("We should have counted 100 elements.") 287 | } 288 | } 289 | 290 | func TestBufferedIterator(t *testing.T) { 291 | m := New[Animal]() 292 | 293 | // Insert 100 elements. 294 | for i := 0; i < 100; i++ { 295 | m.Set(strconv.Itoa(i), Animal{strconv.Itoa(i)}) 296 | } 297 | 298 | counter := 0 299 | // Iterate over elements. 300 | for item := range m.IterBuffered() { 301 | val := item.Val 302 | 303 | if (val == Animal{}) { 304 | t.Error("Expecting an object.") 305 | } 306 | counter++ 307 | } 308 | 309 | if counter != 100 { 310 | t.Error("We should have counted 100 elements.") 311 | } 312 | } 313 | 314 | func TestClear(t *testing.T) { 315 | m := New[Animal]() 316 | 317 | // Insert 100 elements. 318 | for i := 0; i < 100; i++ { 319 | m.Set(strconv.Itoa(i), Animal{strconv.Itoa(i)}) 320 | } 321 | 322 | m.Clear() 323 | 324 | if m.Count() != 0 { 325 | t.Error("We should have 0 elements.") 326 | } 327 | } 328 | 329 | func TestIterCb(t *testing.T) { 330 | m := New[Animal]() 331 | 332 | // Insert 100 elements. 333 | for i := 0; i < 100; i++ { 334 | m.Set(strconv.Itoa(i), Animal{strconv.Itoa(i)}) 335 | } 336 | 337 | counter := 0 338 | // Iterate over elements. 339 | m.IterCb(func(key string, v Animal) { 340 | counter++ 341 | }) 342 | if counter != 100 { 343 | t.Error("We should have counted 100 elements.") 344 | } 345 | } 346 | 347 | func TestItems(t *testing.T) { 348 | m := New[Animal]() 349 | 350 | // Insert 100 elements. 351 | for i := 0; i < 100; i++ { 352 | m.Set(strconv.Itoa(i), Animal{strconv.Itoa(i)}) 353 | } 354 | 355 | items := m.Items() 356 | 357 | if len(items) != 100 { 358 | t.Error("We should have counted 100 elements.") 359 | } 360 | } 361 | 362 | func TestConcurrent(t *testing.T) { 363 | m := New[int]() 364 | ch := make(chan int) 365 | const iterations = 1000 366 | var a [iterations]int 367 | 368 | // Using go routines insert 1000 ints into our map. 369 | go func() { 370 | for i := 0; i < iterations/2; i++ { 371 | // Add item to map. 372 | m.Set(strconv.Itoa(i), i) 373 | 374 | // Retrieve item from map. 375 | val, _ := m.Get(strconv.Itoa(i)) 376 | 377 | // Write to channel inserted value. 378 | ch <- val 379 | } // Call go routine with current index. 380 | }() 381 | 382 | go func() { 383 | for i := iterations / 2; i < iterations; i++ { 384 | // Add item to map. 385 | m.Set(strconv.Itoa(i), i) 386 | 387 | // Retrieve item from map. 388 | val, _ := m.Get(strconv.Itoa(i)) 389 | 390 | // Write to channel inserted value. 391 | ch <- val 392 | } // Call go routine with current index. 393 | }() 394 | 395 | // Wait for all go routines to finish. 396 | counter := 0 397 | for elem := range ch { 398 | a[counter] = elem 399 | counter++ 400 | if counter == iterations { 401 | break 402 | } 403 | } 404 | 405 | // Sorts array, will make is simpler to verify all inserted values we're returned. 406 | sort.Ints(a[0:iterations]) 407 | 408 | // Make sure map contains 1000 elements. 409 | if m.Count() != iterations { 410 | t.Error("Expecting 1000 elements.") 411 | } 412 | 413 | // Make sure all inserted values we're fetched from map. 414 | for i := 0; i < iterations; i++ { 415 | if i != a[i] { 416 | t.Error("missing value", i) 417 | } 418 | } 419 | } 420 | 421 | func TestJsonMarshal(t *testing.T) { 422 | SHARD_COUNT = 2 423 | defer func() { 424 | SHARD_COUNT = 32 425 | }() 426 | expected := "{\"a\":1,\"b\":2}" 427 | m := New[int]() 428 | m.Set("a", 1) 429 | m.Set("b", 2) 430 | j, err := json.Marshal(m) 431 | if err != nil { 432 | t.Error(err) 433 | } 434 | 435 | if string(j) != expected { 436 | t.Error("json", string(j), "differ from expected", expected) 437 | return 438 | } 439 | } 440 | 441 | func TestKeys(t *testing.T) { 442 | m := New[Animal]() 443 | 444 | // Insert 100 elements. 445 | for i := 0; i < 100; i++ { 446 | m.Set(strconv.Itoa(i), Animal{strconv.Itoa(i)}) 447 | } 448 | 449 | keys := m.Keys() 450 | if len(keys) != 100 { 451 | t.Error("We should have counted 100 elements.") 452 | } 453 | } 454 | 455 | func TestMInsert(t *testing.T) { 456 | animals := map[string]Animal{ 457 | "elephant": {"elephant"}, 458 | "monkey": {"monkey"}, 459 | } 460 | m := New[Animal]() 461 | m.MSet(animals) 462 | 463 | if m.Count() != 2 { 464 | t.Error("map should contain exactly two elements.") 465 | } 466 | } 467 | 468 | func TestFnv32(t *testing.T) { 469 | key := []byte("ABC") 470 | 471 | hasher := fnv.New32() 472 | _, err := hasher.Write(key) 473 | if err != nil { 474 | t.Errorf(err.Error()) 475 | } 476 | if fnv32(string(key)) != hasher.Sum32() { 477 | t.Errorf("Bundled fnv32 produced %d, expected result from hash/fnv32 is %d", fnv32(string(key)), hasher.Sum32()) 478 | } 479 | 480 | } 481 | 482 | func TestUpsert(t *testing.T) { 483 | dolphin := Animal{"dolphin"} 484 | whale := Animal{"whale"} 485 | tiger := Animal{"tiger"} 486 | lion := Animal{"lion"} 487 | 488 | cb := func(exists bool, valueInMap Animal, newValue Animal) Animal { 489 | if !exists { 490 | return newValue 491 | } 492 | valueInMap.name += newValue.name 493 | return valueInMap 494 | } 495 | 496 | m := New[Animal]() 497 | m.Set("marine", dolphin) 498 | m.Upsert("marine", whale, cb) 499 | m.Upsert("predator", tiger, cb) 500 | m.Upsert("predator", lion, cb) 501 | 502 | if m.Count() != 2 { 503 | t.Error("map should contain exactly two elements.") 504 | } 505 | 506 | marineAnimals, ok := m.Get("marine") 507 | if marineAnimals.name != "dolphinwhale" || !ok { 508 | t.Error("Set, then Upsert failed") 509 | } 510 | 511 | predators, ok := m.Get("predator") 512 | if !ok || predators.name != "tigerlion" { 513 | t.Error("Upsert, then Upsert failed") 514 | } 515 | } 516 | 517 | func TestKeysWhenRemoving(t *testing.T) { 518 | m := New[Animal]() 519 | 520 | // Insert 100 elements. 521 | Total := 100 522 | for i := 0; i < Total; i++ { 523 | m.Set(strconv.Itoa(i), Animal{strconv.Itoa(i)}) 524 | } 525 | 526 | // Remove 10 elements concurrently. 527 | Num := 10 528 | for i := 0; i < Num; i++ { 529 | go func(c *ConcurrentMap[string, Animal], n int) { 530 | c.Remove(strconv.Itoa(n)) 531 | }(&m, i) 532 | } 533 | keys := m.Keys() 534 | for _, k := range keys { 535 | if k == "" { 536 | t.Error("Empty keys returned") 537 | } 538 | } 539 | } 540 | 541 | func TestUnDrainedIter(t *testing.T) { 542 | m := New[Animal]() 543 | // Insert 100 elements. 544 | Total := 100 545 | for i := 0; i < Total; i++ { 546 | m.Set(strconv.Itoa(i), Animal{strconv.Itoa(i)}) 547 | } 548 | counter := 0 549 | // Iterate over elements. 550 | ch := m.Iter() 551 | for item := range ch { 552 | val := item.Val 553 | 554 | if (val == Animal{}) { 555 | t.Error("Expecting an object.") 556 | } 557 | counter++ 558 | if counter == 42 { 559 | break 560 | } 561 | } 562 | for i := Total; i < 2*Total; i++ { 563 | m.Set(strconv.Itoa(i), Animal{strconv.Itoa(i)}) 564 | } 565 | for item := range ch { 566 | val := item.Val 567 | 568 | if (val == Animal{}) { 569 | t.Error("Expecting an object.") 570 | } 571 | counter++ 572 | } 573 | 574 | if counter != 100 { 575 | t.Error("We should have been right where we stopped") 576 | } 577 | 578 | counter = 0 579 | for item := range m.IterBuffered() { 580 | val := item.Val 581 | 582 | if (val == Animal{}) { 583 | t.Error("Expecting an object.") 584 | } 585 | counter++ 586 | } 587 | 588 | if counter != 200 { 589 | t.Error("We should have counted 200 elements.") 590 | } 591 | } 592 | 593 | func TestUnDrainedIterBuffered(t *testing.T) { 594 | m := New[Animal]() 595 | // Insert 100 elements. 596 | Total := 100 597 | for i := 0; i < Total; i++ { 598 | m.Set(strconv.Itoa(i), Animal{strconv.Itoa(i)}) 599 | } 600 | counter := 0 601 | // Iterate over elements. 602 | ch := m.IterBuffered() 603 | for item := range ch { 604 | val := item.Val 605 | 606 | if (val == Animal{}) { 607 | t.Error("Expecting an object.") 608 | } 609 | counter++ 610 | if counter == 42 { 611 | break 612 | } 613 | } 614 | for i := Total; i < 2*Total; i++ { 615 | m.Set(strconv.Itoa(i), Animal{strconv.Itoa(i)}) 616 | } 617 | for item := range ch { 618 | val := item.Val 619 | 620 | if (val == Animal{}) { 621 | t.Error("Expecting an object.") 622 | } 623 | counter++ 624 | } 625 | 626 | if counter != 100 { 627 | t.Error("We should have been right where we stopped") 628 | } 629 | 630 | counter = 0 631 | for item := range m.IterBuffered() { 632 | val := item.Val 633 | 634 | if (val == Animal{}) { 635 | t.Error("Expecting an object.") 636 | } 637 | counter++ 638 | } 639 | 640 | if counter != 200 { 641 | t.Error("We should have counted 200 elements.") 642 | } 643 | } 644 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/orcaman/concurrent-map/v2 2 | 3 | go 1.18 4 | --------------------------------------------------------------------------------