并发 Go 程序中的共享变量 (二):锁

栏目: Go · 发布时间: 6年前

内容简介:本系列是阅读 “The Go Programming Language” 理解和记录。上一节我们提到了避免 data race 的一种方法是使用 lock,而 Go 的每当有 goroutine 需要获取 balance,首先需要通过 mutex 获得一个排他锁,如果其他的 goroutine 已经获得了 lock,当前 goroutine 会被阻塞直到这个锁被释放。Mutex lock 就是通过这种机制的来保护共享数据的安全的,我们把锁保护起来的区域称之为

本系列是阅读 “The Go Programming Language” 理解和记录。

上一节我们提到了避免 data race 的一种方法是使用 lock,而 GoMutex type 正好就提供了能够满足需要的 lock,直接看例子:

import "sync"

var (
    mu    sync.Mutex
    balance int
)

func Deposit(amountint){
    mu.Lock()
    balance = balance + amount
    mu.Unlock()
}

func Balance()int {
    mu.Lock()
    b := balance
    mu.Unlock()
    return b
}

每当有 goroutine 需要获取 balance,首先需要通过 mutex 获得一个排他锁,如果其他的 goroutine 已经获得了 lock,当前 goroutine 会被阻塞直到这个锁被释放。Mutex lock 就是通过这种机制的来保护共享数据的安全的,我们把锁保护起来的区域称之为 critical section

通过锁来保护 shared data 是一种很常用的机制,其重点在于判别 critical section适时释放锁 ,在上面的例子中锁的释放是在完成 balance 的操作之后开始释放,由于代码量很少,这样的写法并不会引起太大的问题。在复杂的程序中,critical section 的逻辑可能会很复杂,特别是在 critical section 发生错误需要提前返回这时候就需要对 锁进行提前释放 ,因此合理安排 lock 和 unlock 的出现时机变得非常重要,幸运地是 Go 有 defer 语句可以很好的解决这个问题:借助 defer,unlock 能够在 critical section 正确结束或错误返回包括 panic 都能得到执行。

func Balance()int {
    mu.Lock()
    defer mu.Unlock()
    return balance
}

除了锁的释放时机,critical section 的判定更是直接决定了程序能否按照正确的逻辑执行,一起开下面的例子。

func Withdraw(amountint)bool {
    Deposit(-amount)
    if Balance() < 0 {
        Deposit(amount)
        return false // insufficient funds
    }
    return true
}

DepositBalance 是上面我们加了锁的两个函数,虽然 Withdraw 的执行不会造成 balance 无故消失的错误但是却导致了另外一个问题,考虑 Withdraw 在多个 goroutine 中执行,如果一个 goroutine 执行的时候导致 balance 是负的,则会导致另一个 goroutine 逻辑不能正确执行。比如现实中 balance 有 100 ,有一个 goroutine 发起了 110 withdraw 操作,导致 balance 是负的,而另一个 goroutine 即使发起的是 10 withdraw 操作也会失败,虽然 balance 的最终结果是对的,但一个合法的 withdraw 却失败了,这在现实中是无法接受的,就好比明明账上有 100 却无法支付一顿 10 的早餐。导致这个错误的原因就是: Withdraw 不是原子操作

什么是原子操作 atomic operation?原子操作就是一组操作,要么全部执行要么全部不执行,不会发生部分执行,部分不执行的情况。 Withdraw 函数中的一些列操作虽然用 lock 锁住了,但是这些步骤是割裂的,并不是连续的,这样会导致 Withdraw 在并发执行过程中,其它的 goroutine 能够看到当前 Withdraw 未执行完成的结果。一个解决办法是 Withdraw 函数也加锁。

func Withdraw(amountint)bool {
    mu.Lock()
    defer mu.Unlock()
    Deposit(-amount)
    if Balance() < 0 {
        Deposit(amount)
        return false // insufficient funds
    }
    return true
}

由于 mutex 是不可重入的,如果 Withdraw 函数也用了 lock 则会发生死锁:

fatal error: all goroutines are asleep - deadlock!

对于上面的问题,一种通用的做法是,把 Deposit 单独实现,一个是具有 lock 的,对外使用,一个是没有 lock 的,供有 lock 的调用:

func Deposit(amountint) {
    mu.Lock()
    deposit(amount)
    mu.Unlock()
}

func Withdraw(amountint)bool {
    mu.Lock()
    defer mu.Unlock()
    deposit(-amount)
    if balance < 0 {
        deposit(amount)
        return false // insufficient funds
    }
    return true
}

func deposit(amountint) {
    balance = balance + amount
}

Withdraw 例子恰好说明了在使用锁的时候需要考虑 critical section ,这在任何时候使用 mutex 时都需要注意。


以上所述就是小编给大家介绍的《并发 Go 程序中的共享变量 (二):锁》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

重构

重构

[美]马丁•福勒(Martin Fowler) / 熊节 / 人民邮电出版社 / 2015-8 / 69.00

本书清晰揭示了重构的过程,解释了重构的原理和最佳实践方式,并给出了何时以及何地应该开始挖掘代码以求改善。书中给出了70 多个可行的重构,每个重构都介绍了一种经过验证的代码变换手法的动机和技术。本书提出的重构准则将帮助你一次一小步地修改你的代码,从而减少了开发过程中的风险。一起来看看 《重构》 这本书的介绍吧!

RGB转16进制工具
RGB转16进制工具

RGB HEX 互转工具

MD5 加密
MD5 加密

MD5 加密工具

SHA 加密
SHA 加密

SHA 加密工具