Are Golang Maps Thread-Safe?
Language
- unknown
by James Smith (Golang Project Structure Admin)
Concurrency is such an integral part of Golang, because modern computers tend to have multiple processors and threads that code can run on. It’s therefore important that the data structures we use can be used across multiple threads — or, in our language, goroutines.
A map in Golang is equivalent to a dictionary in Python or an object in JavaScript. It simply allows us to store data as key-value pairs.
Table of Contents
Initalizing Maps
Before it can be used, each map has to be initialized with the make
function. The key-type goes inside the square brackets and the value-type follows them.
So in the example below, we’re storing an integer for each string. Let’s say that we’re counting the number of cats and dogs that are housed in kennels that we run.
package main
import (
"fmt"
)
func main() {
m := make(map[string]int)
m["dog"] = 100
m["cat"] = 101
fmt.Println(m) // map[cat:101 dog:100]
}
Reading Is Easy
We can read the map concurrently. For example, we could start five goroutines and get them each to print out the stringified map, and that wouldn’t pose a problem.
package main
import (
"fmt"
"sync"
)
func main() {
m := make(map[string]int)
m["dog"] = 100
m["cat"] = 101
var wg sync.WaitGroup
for i := 1; i <= 5; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
fmt.Println(i, m)
}(i)
}
wg.Wait()
}
We use the sync.WaitGroup
data structure, so that the main function doesn’t end until all the goroutines that are started in the for-loop have completed.
Each goroutine calls a method on the WaitGroup
to show that it has finished: it does this using the defer
keyword, so that the other code in the goroutine is run first.
Writing With a Mutex
However, we cannot safely write to maps concurrently. So in the example below where we start up goroutines to increment the map values, it’s necessary to use a mutex.
Note that we use a defer
statement to trigger an automatic unlock of the mutex when the function ends. Also note that we still don’t need to lock the mutex when we’re reading — unless another goroutine may be writing at the same time, which may often be the case in real-world code.
package main
import (
"fmt"
"sync"
)
func main() {
m := make(map[string]int)
var mutex sync.Mutex
var wg sync.WaitGroup
for i := 0; i < 100; i++ {
wg.Add(1)
go func() {
defer wg.Done()
defer mutex.Unlock()
mutex.Lock()
m["dog"]++
}()
}
for i := 0; i < 101; i++ {
wg.Add(1)
go func() {
defer wg.Done()
defer mutex.Unlock()
mutex.Lock()
m["cat"]++
}()
}
wg.Wait()
fmt.Println(m)
}
Writing With a Synchronized Map
Because concurrent goroutines are used so commonly in Go, the standard library has a specialized data structure that acts like a normal map, but is perfectly safe to be read and written to across multiple goroutines without the use of an external mutex.
The sync.Map
type stores keys and values of any type (using the empty interface).
In the example below, we also use the sync/atomic
package, so that we can increment values in different goroutines without running into a data race.
package main
import (
"fmt"
"sync"
"sync/atomic"
)
func main() {
var m sync.Map
var wg sync.WaitGroup
m.Store("dog", new(int64))
m.Store("cat", new(int64))
for i := 0; i < 100; i++ {
wg.Add(1)
go func() {
defer wg.Done()
if ptrInterface, ok := m.Load("dog"); ok {
if ptr, ok := ptrInterface.(*int64); ok {
atomic.AddInt64(ptr, 1)
}
}
}()
}
for i := 0; i < 101; i++ {
wg.Add(1)
go func() {
defer wg.Done()
if ptrInterface, ok := m.Load("cat"); ok {
if ptr, ok := ptrInterface.(*int64); ok {
atomic.AddInt64(ptr, 1)
}
}
}()
}
wg.Wait()
m.Range(func(key, value interface{}) bool {
if ptr, ok := value.(*int64); ok {
fmt.Println(key, *ptr)
}
return true
})
}
Conclusion
Using a map is an easy way to store values associated with keys. If you only have a main goroutine, you don’t need to worry about concurrency issues. Likewise, if you only ever write to each map from a single goroutine, you’ll be fine.
However, if you want to access the same map across multiple goroutines, then you will need to take some action in order to make sure your data does not get corrupted: the two ways we have seen to do that in this blog post involve using either a normal map with a mutex or a specialized synchronized map from the standard library.