如果两个或者多个goroutine
在没有相互同步状态的情况下同时访问某个资源,并且同时对这个资源进行读写的时候,对于这个资源就处于相互竞争状态(race candition)。下面来看一个相互竞争的例子。
var number int
var wait sync.WaitGroup
func main() {
wait.Add(2)
go updateNumber(20000)//加20000
go updateNumber(30000)//加30000
wait.Wait()
fmt.Println(number)
}
func updateNumber(addNumber int) {
for i:=0;i<addNumber ;i++ {
number ++
}
wait.Done()
}
上面这个例子,我们期望得到的值应该是500000
,但是我们最后得到值,并不是500000
,而且每次得到的结果是不一样的。这是为什么呢?因为在两个goroutine
中没有同步number
的当前值,就会存在两个goroutine
对number
值重复赋值的问题,造成值覆盖。这样就得不到我们预期的结果。
上面的例子我们可以看到,如果没有对竞争的资源进行有效的管理以及合理的处理,并发程序就会变的很复杂,并且会产生一些意想不到的错误。所以我们需要对竞争资源进行管理来避免这些问题。Go
中提供一些传统的方式来处理这类问题
原子函数 atomic
原子函数能够以很底层的加锁机制来同步访问整型变量和指针,我们可以使用原子函数来处理竞争问题。
var number int32
var wait sync.WaitGroup
func main() {
wait.Add(2)
go updateNumber(20000)
go updateNumber(30000)
wait.Wait()
fmt.Println(number)
}
func updateNumber(addNumber int) {
defer wait.Done()
for i:=0;i<addNumber ;i++ {
atomic.AddInt32(&number,1)
}
}
这里我们使用了atmoic
包的AddInt32
函数。这个函数会同步整型值的加法,
方法是强制同一时刻只能有一个goroutine
运行并完成这个加法操作。当goroutine
试图去调用任
何原子函数时,这些goroutine
都会自动根据所引用的变量做同步处理。atmoic
包中还提供了Load
与Store
方法,对资源进行安全的读与写。
互斥锁 mutex
另一种方式是创建一个互斥锁来锁住一个区域,来保证同一个资源不会被同时修改或者使用。保证当前只有一个goroutine
在执行当前区域的代码。
var (
number int32
wait sync.WaitGroup
mutex sync.Mutex
)
func main() {
wait.Add(2)
go updateNumber(20000)
go updateNumber(30000)
wait.Wait()
fmt.Println(number)
}
func updateNumber(addNumber int) {
defer wait.Done()
for i:=0;i<addNumber ;i++ {
mutex.Lock() // 加锁
number++;
mutex.Unlock() //释放锁
}
}
上面的代码片段,在Number
改变的前后对当前区域加锁,最后也能得到我们的目的,但是这样的会话,每次在number
变更的时候,都会创建锁与释放锁,会对性能产生很大的影响。其实我们可以在for
循环区域来加锁。
func updateNumber(addNumber int) {
defer wait.Done()
mutex.Lock() // 加锁
for i:=0;i<addNumber ;i++ {
number++;
}
mutex.Unlock() //释放锁
}
后面这种形式的效率明显是要比第一种高很多的。所以我们程序有使用互斥锁的话,需要考虑加锁的粒度问题。
虽然上面上面两种方式也可以解决竞争问题,但是在go
中有一种更好的方式来解决这个问题,那就是goroutine
的好兄弟channel
。由于channel
的内容比较多,所以我将单独写一个笔记来记录这方面的问题。期待下一篇的更新
期待一起交流
有疑问加站长微信联系(非本文作者)