goroutine并发扫描MySQL表_Go并发的数据竞争

发生数据竞争的条件

在一个线程程序中(或者说只有一个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的读操作,在主存同步之前是不可见的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值