05/02/2020 sync.map map concurrency
Let's take a look to sync.Map
usage and it's sources.
Far ago in year 2016, GO 1.9 was released with
new sync.Map
type.
Following methods are available to work with sync.Map
:
Load(key interface{}) (value interface{}, ok bool)
Store(key, value interface{})
Delete(key interface{})
Range(f func(key, value interface{}) bool)
LoadOrStore(key, value interface{}) (actual interface{}, loaded bool)
That covers every map use case — insert, read, delete, iterating.
Also there is LoadOrStore
bonus method, that
allows to set value by key if value is not set before.
To perform iterating there is Range
,
receiving function that is called on every map element.
If that function returns false
, iterating is going to be stopped.
When seeing sync.Map
name one
can imagine that sync.Map
is simple map with some sync
package features.
But actually sync.Map
is much more complex.
Let's dive into the sources and find out how it works.
sync.Map
struct is looks as below:
type Map struct { mu sync.Mutex read atomic.Value dirty map[interface{}]*entry misses int } type readOnly struct { m map[interface{}]*entry amended bool }
read
— pointer to readOnly
struct. This struct stores
part of the data, that is used to read data by a key or to check that there is data behind that key.
There is no inserts into read
, that's why no mutex is used accessing read
.
dirty
— a map, that stores other part of elements.
It is used to place new elements generally and for read too.
In that case mu
mutex is used when dirty
is accessed.
So sync.Map
has two internal maps (read
and dirty
).
By using one map only for reading and second for writing sync.Map
trying to avoid using mutex
when it is possible.
Let's take a look to sync.Map
operations.
At first read
is accessed to check if there is a data behind the key.
If it is — the data is returned.
It is the most simple scenario — to return data from read
.
Then dirty
is searched — the mu
mutex is activated before.
misses
— counter of access to dirty
— is incremented.
Important thing — if that counter's value is more that number of element in dirty
,
elements in dirty
will be copied to read
, counter will be reset.
The copying will happen right after dirty
access — mu
is activated
at that moment.
To avoid extra dirty
access, there is
amended
field in readOnly
struct (which is read
).
amended
= true means there is non-empty dirty
map in sync.Map
.
So, reading from sync.Map
is most effective on read
only.
In this case this reading is equal to simple map (without mutex) reading.
If there is only reading from a large map set up before,
sync.Map
is good in that case.
At first there is check against read
— if there is a data behind the key.
If yes — the value is updated by atomic.CompareAndSwap
.
If there is no data read
by the key,
the same reading is performed against dirty
.
If dirty
is not initialized — it will be.
So, updating already existent key is simple, but adding data with new key is complex.
Before iterating dirty
is copied into read
,
if dirty
stores anything.
It is made with mu
mutex activated.
Next, there is read
iterating — simple map iterating.
sync.Map
is complex struct, generally consisting
of two map — one for reading and one for new elements.
sync.Map
is not only close to map+sync.RWMutex
on
speed metrics, but also can be the best in the reading case.
When there is both reading and updating sync.Map
will have
element in both read
and dirty
.
In that case sync.Map
performs worse than map+sync.RWMutex
because of reading from two internal maps.
Also there is a counter that needs to be update when accessing dirty
.