075-互斥锁

Posted --Allen--

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了075-互斥锁相关的知识,希望对你有一定的参考价值。

过去我们在写 C/C++ 程序的时候,总会听到竞争的概念。比如多线程对共享变量的存取,需要加锁才能避免数据不致的问题。

在 Golang 中也不例外,多个 Goroutine 存取共享变量,同样也会出现竞争问题。

1. 银行转账

假设 Alice 有一个银行账户。Alice 会往里存钱,她的好友 Bob 也会转钱给她。用程序来描述应该是这样:

package bank

// Alice 的账户余额
var balance int

// 存钱
func Deposit(amount int) 
    balance = balance + amount


// 查看余额
func Balance() int 
    return balance

有了以上的模拟存钱的函数后,Alice 和 Bob 就可以存钱啦。比如:

// 存入 200
bank.Deposit(200)
  • 单 goroutine 顺序存取

如果 Alice 存入 200,Bob 存入 100,那看起来就像这样:

func alice(amount int) 
    fmt.Printf("Alice deposit $%d\\n", amount)
    bank.Deposit(amount)


func bob(amount int) 
    fmt.Printf("Bob deposit $%d\\n", amount)
    bank.Deposit(amount)


func main() 
    alice(200);
    bob(100);
    fmt.Printf("balance: $%d\\n", bank.Balance()) // balance: 300
  • 并发存取

看起来我们的程序似乎也没什么问题。但是考虑一下,Alice 和 Bob 是两个人,他们有可能会同时往账户里存钱,也就是说,Alice 和 Bob 存钱的时候 ,调用 Deposit 函数是并发的。

func main() 
    var wg sync.WaitGroup
    wg.Add(2)

    go func() 
        alice(100)
        wg.Done()
    ()
    go func() 
        bob(200)
        wg.Done()
    ()

    wg.Wait()

    fmt.Printf("balance: $%d\\n", bank.Balance()) // balance: 300

这个时候,余额还能是 300 吗?

2. 竞争分析

我们重点看存钱的操作,这是并发的。

// 存钱
func Deposit(amount int) 
    balance = balance + amount

假设 Alice 在存钱的时候,语句 balance = balance + amount 执行到一半,即 balance + amount 的结果计算出来,是 200,并赋值给 balance 的时候,Goroutine 被切换出去。

这时候执行 Bob 的 Goroutine,Bob 存入了 100,完成后,Alice 的 Goroutine 又切换回来,继续执行之前的赋值操作,将 200 赋值给 balance。

最后的结果是 200,而不是 300.

有一定基础的同学分析起这个竞态条件非常简单,因为 balance 是全局共享的。Deposit 函数引用了全局共享的变量,因此 Deposit 也是协程不安全函数。

3. 解决方案

3.1 使用 channel

为了解决这种协程不安全问题,对 balance 一定要互斥访问。前面我们学过了 channel,这很容易做到。

package bank

// Alice 的账户余额
var balance int
// channel 信号量
var sema = make(chan struct, 1)

// 存钱
func Deposit(amount int) 
    sema <- struct
    balance = balance + amount
    <-sema


// 查看余额
// 你可能会好奇,为什么这里对 balance 的读取也会加锁,直接 return balance 不就好了吗?
// 这是一个更加深入的话题,我们后面会继续讨论。
func Balance() int 
    sema <- struct
    b := balance
    <-sema
    return b

3.2 使用互斥锁

实际上在 Golang 里提供了更加方便的结构来帮助我们做这件事,sync.Mutex. 这和你在其它地方看到的 Mutex 是一个概念。我们使用 Mutex 来重写上面的程序。

package bank

import "sync"

// Alice 的账户余额
var balance int
// channel 信号量
var mu sync.Mutex

// 存钱
func Deposit(amount int) 
    mu.Lock()
    defer mu.Unlock()
    balance = balance + amount


// 查看余额
func Balance() int 
    mu.Lock()
    defer mu.Unlock()
    return balance

由于 sync.Mutex 提供了 Lock 和 Unlock 方法,我们可以使用 defer 来搭配 Lock 和 Unlock,这样看起来会更加简洁。

4. 总结

实际上,我们并不推荐在 Golang 里使用互斥量。Golang 的口头禅是:

Do not communicate by sharing memory; instead, share memory by communicating.

“不要使用共享内存进行通信,应该使用通信来共享数据”。下一节,我们来演示如何使用通信的方式来共享数据。

以上是关于075-互斥锁的主要内容,如果未能解决你的问题,请参考以下文章

为啥不可能将互斥锁传递给线程?

你了解多线程自旋锁互斥锁递归锁等锁吗?

为啥互斥锁不需要互斥锁(而互斥锁需要互斥锁......)

并发编程互斥锁

使用条件变量优于互斥锁的优点

自旋锁代替互斥锁的实践