我们先来看一个例子,多个协程修改同一个资源会发生什么?

如上图所示,多个协程对同一个资源进行修改,可能出现竞争关系,导致数据被覆盖,得到不想要的结果。

原因:
我们修改一个变量的过程是分成了三个步骤:

  1. 创建一个副本,从内存中拷贝数据到副本
  2. 对副本进行相加计算
  3. 把副本的值拷贝到内存中,覆盖原来的值

我们可以看到这三个操作并不是原子性的,可能出现协程1获取到数据后,还没来得及修改数据就被协程2给覆盖了。

在golang中,我们可以通过以下三种方式进行保护共享资源,处理race condition。

1、通过sync.Mutex进行加锁
var mtx sync.Mutex
var wg sync.WaitGroup
var counter int32

func main() {
	wg.Add(2)

	go mutexIncrCounter()
	go mutexIncrCounter()

	wg.Wait()
	fmt.Println("counter: ", counter)
}

// 通过mutex加锁
func mutexIncrCounter() {
	defer wg.Done()

	for i := 0; i < 1000; i++ {
		mtx.Lock()
		counter++
		mtx.Unlock()
	}
}

// 运行结果 counter:  2000
2、使用atomic进行原子操作
var wg sync.WaitGroup
var counter int32

func main() {
	wg.Add(2)

	go atomicIncrCounter()
	go atomicIncrCounter()

	wg.Wait()
	fmt.Println("counter: ", counter)
}

// 通过atomic包实现原子操作
func atomicIncrCounter() {
	defer wg.Done()

	for i := 0; i < 1000; i++ {
		atomic.AddInt32(&counter, 1)
	}
}
// 运行结果 counter:  2000
3、利用channel进行数据同步(channel底层实现时用到了互斥锁和缓冲数据队列,在元素进行出队/入队时都通过锁机制保障了操作的原子性,避免了复杂的竞态情形)
var wg sync.WaitGroup
var counter int32
var ch = make(chan int32, 0)

func main() {
	wg.Add(2)

	go channelIncrCounter()
	go channelIncrCounter()

	ch <- 0

	wg.Wait()
	fmt.Println("counter: ", counter)
}

// 通过channel处理race condition
func channelIncrCounter() {
	defer wg.Done()

	for i := 0; i < 1000; i++ {
		counter = <-ch
		counter++
		ch <- counter
	}
	_, ok := <-ch
	if ok {
		close(ch)
	}
}
// 运行结果 counter:  2000