发生数据竞争的条件
在一个线程程序中(或者说只有一个goroutine),程序的执行顺序是由程序的逻辑来决定。
在两个或以上的goroutine的程序中,每一个goroutine内也是按照既定的顺序去执行语句,但一般情况下是无法知道分别位于两个goroutine中的x事件和y事件的执行顺序,当我们无法确定x事件是在y之前还是之后还是同时发生的,就说明这两事件是并发的。
只有文档中明确说了某函数或类型是并发安全,才能并发地使用;包级(package)变量没法像局部变量可以限制在单一goroutine内使用,要修改这些变量必须使用互斥条件。
只要并发(多个goroutine)地访问同一变量,且其中至少有一个是写操作时就会发生数据竞争。
避免数据竞争的方法避免多个goroutine访问变量。
将需要共享的变量限制为单独goroutine内的局部变量,其他goroutine不能直接访问变量,只能使用一个channel发送给指定goroutine来访问。这是Go的口头禅:不要使用共享数据来通信,使用通信来共享数据。
package bank
var deposits = make(chan int) //存款通道,存款是向通道发送数据的行为var balances = make(chan int) //余额通道,查询余额是从通道读取数据的行为
func Deposit(amount int) { deposits
func Balance() int { return
func teller() {
var balance int // balance是局部变量,被限制在goroutine内访问。
for {
select { //监控通道中的就绪事件,外部goroutine只能通过通道来访问局部变量balance case amount :=
case balances
}
}
go teller() //启动监控的goroutine使用锁。在goroutine访问某个资源时先锁住,防止其它goroutine的访问,等到访问完毕解锁后其他协程再来加锁进行访问。
Go实现了两种锁Mutex (互斥锁)和RWMutex(读写锁),RWMutex是基于Mutex使用类似引用计数器的功能实现。
Mutex互斥锁,适用于同一时刻只能有一个读或写的场景。
var (
mu sync.Mutex
balance int
)
func Deposit(amount int) {
mu.Lock()
defer mu.Unlock()
/*..../*
return balance
}
每一个goroutine访问balance变量,都要先获得Mutex互斥锁,如果其他goroutine已经获得锁,当前goroutine会一直阻塞直到其他goroutine释放锁。
持有锁的goroutine在适用结束后必须释放锁。
使用defer释放锁,在函数返回之后或在发生错误返回时会自动调用Unlock。使用defer会比显式调用Unlock释放锁的成本高一点点,但大多数情况下对于并发程序来说,代码整洁性比过度优化更重要。
RWMutex读写锁,允许有多个读锁,但只能有一个写锁。适用于“多读少写”场景。
Lock() 写锁。在申请锁时,如果已被其他的读锁或写锁获得,Lock会阻塞直到其他goroutine释放锁。如果已阻塞的Lock调用有多个申请锁,写锁会优先锁定,即写锁权限高于读锁。
RLock() 读锁。当有写锁时,没法获得读锁;当只有读锁或无锁时,可以获取多个读锁。
Go没有重入锁,对一个已经上锁的mutex再次上锁会导致程序死锁,通用的做法是将一个函数分解为多个函数。比如将Deposit分离成一个包内函数deposit,负责执行实际的操作,另一个可被包外调用的Deposit,这个函数会先获得锁后再去调用deposit。
func Withdraw(amount int) bool {
mu.Lock()
defer mu.Unlock()
deposit(-amount)
if balance < 0 {
deposit(amount)
return false
}
return true
}
func Deposit(amount int) {
mu.Lock()
defer mu.Unlock()
deposit(amount)
}
// 假设调用函数会被上锁func deposit(amount int) { balance += amount }
内存同步
计算机中每个处理器都会有一个本地缓存。为了效率,对内存的写入一般会在每个处理中缓存,并在必要时更新到主存。这种情况下,数据提交到主存的顺序可能会与当初goroutine写入的顺序不同。
var x, y int
go func() {
x = 1 // A1 fmt.Print("y:", y, " ") // A2}()
go func() {
y = 1 // B1 fmt.Print("x:", x, " ") // B2}()
// 可能会出现几种交错的执行情况:// y:0 x:1// x:0 y:1// x:1 y:1// y:1 x:1
可以尝试将并发的运行理解为在不同的goroutine中交错执行语句。虽然goroutine A会在执行x=1之后再打印y的值,但它没法确保自己可以观察到goroutine B对y的写入,所以A是有可能打印到y的旧值。
然而实际上有可能出现意想不到的情况:
x:0 y:0
y:0 x:0
为了并发最大化,处理器和编译器可能会对执行语句重新排序。因为赋值和打印指向不同的变量,编译器可能会断定两条语句的顺序不会影响执行结果,会交换两个语句的执行顺序。
如果两个goroutine在不同的处理器上执行,每个处理器有自己的缓存,一个goroutine的写入对于其它goroutine的读操作,在主存同步之前是不可见的。