Comparing Go performance of Mutex, sync.Map and Channels for concurrent map access for increments

Nikolay Dimitrov
2 min readFeb 21, 2023

--

We are facing a simple problem here — we want to have a map, and based on the key, we want to be able to increase/count/set the corresponding value.

That is quite trivial, but what is the best approach? What should we use to prevent race conditions? The infamous Go channels? Simple Mutex locking? Or the sync.Map?

EDIT: Adding “github.com/orcaman/concurrent-map/v2”

How is the relative performance of those? Any guesses?
Here’s the small benchmark application I quickly put together:

package main

import (
"fmt"
"strconv"
"sync"
"sync/atomic"
"time"

cmap "github.com/orcaman/concurrent-map/v2"
)

const (
iterations = 1000000
threads = 10
)

type (
data struct {
key int
val int
}
)

var (
cache = make(map[int]int, 100)
syncCache sync.Map
mutex sync.Mutex
cm = cmap.New[int]()
ch = make(chan data)
)

func measure(name string, f func()) {
fmt.Println("Start measuring", name, "...")
start := time.Now()
f()
taken := time.Since(start)
fmt.Printf("Finished measuring %s, time taken: %v\n\n", name, taken)
}

func exec(meth0d func(i int)) {
wg := new(sync.WaitGroup)
wg.Add(threads)
for t := 0; t < threads; t++ {
go func() {
for i := 0; i < iterations; i++ {
meth0d(i)
}
wg.Done()
}()
}
wg.Wait()
}
func main() {
measure("Mutex", func() {
exec(func(i int) {
mutex.Lock()
cache[i%100000] += 1
mutex.Unlock()
})
})
measure("concurrent-map", func() {
exec(func(i int) {
cm.Upsert(strconv.Itoa(i), 1, func(exist bool, valueInMap int, newValue int) int {
return newValue + valueInMap
})
})
})
measure("sync.Map", func() {
exec(func(i int) {
elem, _ := syncCache.LoadOrStore(i, new(int32))
atomic.AddInt32(elem.(*int32), 1)
})
})
measure("Channels", func() {
go func() {
for x := range ch {
cache[x.key] += x.val
}
}()
exec(func(i int) {
ch <- data{i, 1}
})
})
}

And here is the output:

Start measuring Mutex ...
Finished measuring Mutex, time taken: 1.001179811s

Start measuring concurrent-map ...
Finished measuring concurrent-map, time taken: 653.905642ms

Start measuring sync.Map ...
Finished measuring sync.Map, time taken: 3.415632969s

Start measuring Channels ...
Finished measuring Channels, time taken: 3.993437365s

If we increase the number of parallel producers (threads) the results remain relatively the same.

I’d be happy to hear any opinions and also comments about the code or suggestions maybe.

Have a good one! :)

--

--