Skip to content

Commit 6d7cf65

Browse files
authored
Concurrency safe (#7)
* feat: Concurrency improvements These changes make the package safe for concurrent go routines. I tried to minimize lock contention using atomic and RWMutex. Also fixed a bug with maps/slices of Items (exported type), which now are handled correctly. * chore: update copy
1 parent 433dbec commit 6d7cf65

15 files changed

Lines changed: 428 additions & 50 deletions

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
.gocache

LICENSE

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
The MIT License (MIT)
22

3-
Copyright (c) 2019 srfrog - https://srfrog.me
3+
Copyright (c) 2025 srfrog - https://srfrog.dev
44

55
Permission is hereby granted, free of charge, to any person obtaining a copy
66
of this software and associated documentation files (the "Software"), to deal

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,10 @@ View [example_test.go][2] for an extended example of basic usage and features.
2929
- [x] Go types int, uint, float, string and fmt.Stringer are hashable for dict keys.
3030
- [x] Go map keys are used for dict keys if they are hashable.
3131
- [x] Dict items are sorted in their insertion order, unlike Go maps.
32-
- [ ] Go routine safe with minimal mutex locking (WIP)
32+
- [x] Go routine safe with minimal mutex locking (WIP)
3333
- [x] Builtin JSON support for marshalling and unmarshalling
3434
- [ ] sql.Scanner support via optional sub-package (WIP)
35-
- [ ] Plenty of tests and examples to get you started quickly (WIP)
35+
- [x] Plenty of tests and examples to get you started quickly
3636

3737
## Documentation
3838

benchmark_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright (c) 2019 srfrog - https://srfrog.me
1+
// Copyright (c) 2025 srfrog - https://srfrog.dev
22
// Use of this source code is governed by the license in the LICENSE file.
33

44
package dict

conv.go

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright (c) 2019 srfrog - https://srfrog.me
1+
// Copyright (c) 2025 srfrog - https://srfrog.dev
22
// Use of this source code is governed by the license in the LICENSE file.
33

44
package dict
@@ -82,6 +82,14 @@ type Item struct {
8282
func toIterable(i interface{}) <-chan Item {
8383
ci := make(chan Item)
8484

85+
// If the value is an Item, just return it.
86+
transform := func(ii interface{}) Item {
87+
if v, ok := ii.(Item); ok {
88+
return v
89+
}
90+
return Item{Value: ii}
91+
}
92+
8593
go func() {
8694
defer close(ci)
8795

@@ -112,16 +120,16 @@ func toIterable(i interface{}) <-chan Item {
112120
if !ok {
113121
break L
114122
}
115-
ci <- Item{Value: x.Interface()}
123+
ci <- transform(x.Interface())
116124
}
117125

118126
case reflect.Array, reflect.Slice:
119127
for j := 0; j < v.Len(); j++ {
120-
ci <- Item{Key: j, Value: v.Index(j).Interface()}
128+
ci <- transform(v.Index(j).Interface())
121129
}
122130

123131
default:
124-
ci <- Item{Value: v.Interface()}
132+
ci <- transform(v.Interface())
125133
}
126134
}()
127135

dict.go

Lines changed: 89 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,35 @@
1-
// Copyright (c) 2019 srfrog - https://srfrog.me
1+
// Copyright (c) 2025 srfrog - https://srfrog.dev
22
// Use of this source code is governed by the license in the LICENSE file.
33

44
package dict
55

66
import (
77
"fmt"
8+
"reflect"
89
"strings"
10+
"sync"
11+
"sync/atomic"
912
)
1013

1114
// Dict is a type that uses a hash mapping index, also known as a dictionary.
1215
type Dict struct {
13-
size, version int
16+
size, version int64
1417
keys []*Key
1518
values map[uint64]interface{}
19+
mu sync.RWMutex
1620
}
1721

1822
// Version returns the version of the dictionary. The version is increased after every
1923
// change to dict items.
2024
// Returns version, which is zero (0) initially.
21-
func (d *Dict) Version() int { return d.version }
25+
func (d *Dict) Version() int {
26+
return int(atomic.LoadInt64(&d.version))
27+
}
2228

2329
// Len returns the size of a Dict.
24-
func (d *Dict) Len() int { return d.size }
30+
func (d *Dict) Len() int {
31+
return int(atomic.LoadInt64(&d.size))
32+
}
2533

2634
// New returns a new Dict object.
2735
// vargs can be any Go basic type, slices, and maps. The keys in a map are
@@ -45,14 +53,23 @@ func (d *Dict) Set(key, value interface{}) *Dict {
4553
return d
4654
}
4755

48-
if _, ok := d.values[k.ID]; ok {
56+
d.mu.Lock()
57+
defer d.mu.Unlock()
58+
59+
if curr, ok := d.values[k.ID]; ok {
4960
d.values[k.ID] = value
61+
62+
// Value changed, update version.
63+
if !reflect.DeepEqual(value, curr) {
64+
atomic.AddInt64(&d.version, 1)
65+
}
66+
5067
return d
5168
}
5269
d.keys = append(d.keys, k)
5370
d.values[k.ID] = value
54-
d.size++
55-
d.version++
71+
atomic.AddInt64(&d.size, 1)
72+
atomic.AddInt64(&d.version, 1)
5673

5774
return d
5875
}
@@ -64,8 +81,11 @@ func (d *Dict) Get(key interface{}, alt ...interface{}) interface{} {
6481
if d.IsEmpty() {
6582
return nil
6683
}
84+
6785
h, ok := d.GetKeyID(key)
6886
if ok {
87+
d.mu.RLock()
88+
defer d.mu.RUnlock()
6989
return d.values[h]
7090
}
7191
if alt != nil {
@@ -80,16 +100,21 @@ func (d *Dict) GetKeyID(key interface{}) (uint64, bool) {
80100
if d.IsEmpty() {
81101
return 0, false
82102
}
103+
83104
k := MakeKey(key)
84105
if k == nil {
85106
return 0, false
86107
}
108+
109+
d.mu.RLock()
87110
_, ok := d.values[k.ID]
111+
d.mu.RUnlock()
112+
88113
return k.ID, ok
89114
}
90115

91116
func (d *Dict) deleteItem(idx int) {
92-
if d.IsEmpty() || idx >= d.size {
117+
if d.IsEmpty() || idx >= d.Len() {
93118
return
94119
}
95120

@@ -98,8 +123,8 @@ func (d *Dict) deleteItem(idx int) {
98123
l := len(d.keys)
99124
d.keys[l-1] = nil
100125
d.keys = d.keys[:l-1]
101-
d.size = l
102-
d.version++
126+
atomic.StoreInt64(&d.size, int64(l-1))
127+
atomic.AddInt64(&d.version, 1)
103128
}
104129

105130
// Del removes an item from dict by key name.
@@ -110,14 +135,18 @@ func (d *Dict) Del(key interface{}) bool {
110135
return false
111136
}
112137

138+
d.mu.Lock()
139+
defer d.mu.Unlock()
140+
113141
var idx int
114142
for i := range d.keys {
115143
if d.keys[i].ID == id {
116144
idx = i
117145
break
118146
}
119147
}
120-
if idx > d.size || d.keys[idx].ID != id {
148+
149+
if idx >= len(d.keys) || d.keys[idx].ID != id {
121150
return false
122151
}
123152

@@ -143,9 +172,17 @@ func (d *Dict) PopItem() *Item {
143172
return nil
144173
}
145174

146-
key := d.keys[d.size-1]
175+
d.mu.Lock()
176+
defer d.mu.Unlock()
177+
178+
size := len(d.keys)
179+
if size == 0 {
180+
return nil
181+
}
182+
183+
key := d.keys[size-1]
147184
value := d.values[key.ID]
148-
d.deleteItem(d.size - 1)
185+
d.deleteItem(size - 1)
149186

150187
return &Item{
151188
Key: key.Name,
@@ -161,7 +198,7 @@ func (d *Dict) Key(key interface{}) bool {
161198

162199
// IsEmpty returns true if the dict is empty, false otherwise.
163200
func (d *Dict) IsEmpty() bool {
164-
return d == nil || d.size == 0
201+
return d == nil || d.Len() == 0
165202
}
166203

167204
// Clear empties a Dict d.
@@ -170,8 +207,13 @@ func (d *Dict) Clear() bool {
170207
if d.IsEmpty() {
171208
return false
172209
}
173-
d.size = 0
174-
d.version++ // not a new dict
210+
211+
d.mu.Lock()
212+
defer d.mu.Unlock()
213+
214+
atomic.StoreInt64(&d.size, 0)
215+
atomic.AddInt64(&d.version, 1)
216+
175217
d.keys = []*Key{}
176218
d.values = make(map[uint64]interface{})
177219
return true
@@ -182,7 +224,11 @@ func (d *Dict) Keys() []string {
182224
if d.IsEmpty() {
183225
return nil
184226
}
185-
keys := make([]string, d.size)
227+
228+
d.mu.RLock()
229+
defer d.mu.RUnlock()
230+
231+
keys := make([]string, d.Len())
186232
for i := range d.keys {
187233
keys[i] = d.keys[i].Name
188234
}
@@ -194,7 +240,11 @@ func (d *Dict) Values() []interface{} {
194240
if d.IsEmpty() {
195241
return nil
196242
}
197-
values := make([]interface{}, d.size)
243+
244+
d.mu.RLock()
245+
defer d.mu.RUnlock()
246+
247+
values := make([]interface{}, d.Len())
198248
for i, key := range d.keys {
199249
values[i] = d.values[key.ID]
200250
}
@@ -204,17 +254,29 @@ func (d *Dict) Values() []interface{} {
204254
// Items returns a channel of key-value items, or nil if the dict is empty.
205255
func (d *Dict) Items() <-chan Item {
206256
ci := make(chan Item)
257+
if d.IsEmpty() {
258+
close(ci)
259+
return ci
260+
}
261+
262+
// Avoid lock contention
263+
d.mu.RLock()
264+
items := make([]Item, len(d.keys))
265+
for i := range d.keys {
266+
items[i] = Item{
267+
Key: d.keys[i].Name,
268+
Value: d.values[d.keys[i].ID],
269+
}
270+
}
271+
d.mu.RUnlock()
207272

208273
go func() {
209274
defer close(ci)
210-
if d.IsEmpty() {
275+
if len(items) == 0 {
211276
return
212277
}
213-
for i := range d.keys {
214-
ci <- Item{
215-
Key: d.keys[i].Name,
216-
Value: d.values[d.keys[i].ID],
217-
}
278+
for _, item := range items {
279+
ci <- item
218280
}
219281
}()
220282

@@ -241,14 +303,16 @@ func (d *Dict) Update(vargs ...interface{}) bool {
241303
// iterables and scalars
242304
for item := range toIterable(vargs[i]) {
243305
if item.Key == nil {
244-
item.Key = d.size
306+
item.Key = d.Len()
245307
}
246308
d.Set(item.Key, item.Value)
247309
}
248310
}
249311
return ver != d.Version()
250312
}
251313

314+
// String implements the fmt.Stringer interface to print d similar to a Python dict.
315+
// Returns a formatted string with the keys and values of the dict.
252316
func (d *Dict) String() string {
253317
items := make([]string, 0, d.Len())
254318
for item := range d.Items() {

0 commit comments

Comments
 (0)