Golang中的原子操作

Love means never having to say you're sorry.

With it easy to use go routines, you have unbridled power to harness concurrency in your programs. However, Go is not spared from race conditions. We still have to use mutex and atomic constructs to ensure that shared variables and their state are correct when read/write by go routines. The aim of this article is to see what could go wrong if you are not careful and how to avoid race conditions in your code.

Let’s create a simple program to demonstrate a shared variable.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package mutexAtomicExample
import (
"fmt"
"sync"
"sync/atomic"
)
func Increment() {
var count int
var wg sync.WaitGroup //needed so that the function don't
//exit prematurely relative
//to all go routines
for i := 0; i < 100000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
count++
}()
}
wg.Wait()
fmt.Printf("count: %v for 100000 cycles\n", count)
}

In the increment function above, we use create 100,000 go routines where each of these will increment the count variable. We use a waitgroup to ensure the main program stays blocked [at the wg.Wait() line] until all the go routines have the chance to run till completion.

Surprisingly, the output is as follows if we run the increment function.

1
count: 98152 for 100000 cycles

What gives?? Turns out that despite the waitgroup, the increment count operations carried out by each go routine may not be successful. The increment operation is not atomic, meaning that it could be interrupted midway by another go routine that is working concurrently to increment the same count variable. Hence, you won’t have count that will reach 100,000 but less than that. At the high level, this is a race condition.

Two ways to handle — Mutex and Atomic Construct

In the first way, we use mutex to lock the shared variable such that only one go routine can increment the count at a time.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func IncrementMutex() {
var count int
var wg sync.WaitGroup
m := sync.Mutex{} //1
for i := 0; i < 100000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
m.Lock()
defer m.Unlock()
count++
}()
}
wg.Wait()
fmt.Printf("count: %v for 100000 cycles with Mutex\n", count)
}

In //(1), we create a variable mutex with the sync.Mutex{} structure which will give a zero value mutex. This will by default be an unlocked mutex.

In the go routine, we lock the mutex before the increment of the count and unlock thereafter. And below is the output of running the function.

1
count: 100000 for 100000 cycles with Mutex

Now, let’s try to use the atomic construct which is available as part of the sync/atomic. Again we create a variant of the increment function:

1
2
3
4
5
6
7
8
9
10
11
12
13
func IncrementAtomic() {
var count int64 // sync atomic cannot work with int (1)
var wg sync.WaitGroup
for i := 0; i < 100000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
atomic.AddInt64(&count, 1) //(2)
}()
}
wg.Wait()
fmt.Printf("count: %v for 100000 cycles with Atomic\n", count)
}

Notice in (1), we had to use a int64 instead of the int since the atomic package cannot work with int. We can also use int32. Below are some functions showing the specific integer types that will work. So be sure to check the official documentations before using the sync/atomic package.

  • func AddInt32(addr *int32, delta int32) (new int32)
  • func AddInt64(addr *int64, delta int64) (new int64)
  • func AddUint32(addr *uint32, delta uint32) (new uint32)
  • func AddUint64(addr *uint64, delta uint64) (new uint64)
  • func AddUintptr(addr *uintptr, delta uintptr) (new uintptr)

In //(2), we evoke the AddInt64 function which requires the int64 variable as the first parameter and the delta value (also another int64) that we want to add to it.

https://medium.com/@naikofficial56/concurrency-with-golang-7d8e0c65ef85

https://medium.com/@naikofficial56/concurrency-with-golang-7d8e0c65ef85