我们先来看一个例子,多个协程修改同一个资源会发生什么?
如上图所示,多个协程对同一个资源进行修改,可能出现竞争关系,导致数据被覆盖,得到不想要的结果。
原因:
我们修改一个变量的过程是分成了三个步骤:
- 创建一个副本,从内存中拷贝数据到副本
- 对副本进行相加计算
- 把副本的值拷贝到内存中,覆盖原来的值
我们可以看到这三个操作并不是原子性的,可能出现协程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