> For the complete documentation index, see [llms.txt](https://sunwenfei.gitbook.io/sunwenfei/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://sunwenfei.gitbook.io/sunwenfei/golang/golang-ji-chu-jiao-cheng/bing-fa/hu-chi-suo-mutex.md).

# 互斥锁(Mutex)

本节我们主要介绍下`互斥锁`，然后看下如果使用`互斥锁`和`管道`来解决竞态问题。

#### 临界区 & 竞态问题

在讨论互斥锁之前，我们首先需要弄明白`临界区`的概念。临界区属于并发编程的范畴，指的是不同`goroutine`中访问同一个临界资源的那段代码，所谓临界资源是指被所有`goroutine`共享的资源。这就好比多个人同时去争抢一个苹果，肯定会有冲突问题。我们通过一个经典例子来理解下临界区的概念。下面的这行代码很简单，将一个变量递增1。

```go
x = x + 1
```

如果这行代码自始自终仅在一个协程内执行，那么不会有任何的冲突问题。

但是假设有多个并发的协程都会执行该行代码，那么程序逻辑就有可能出现问题。为了简单起见，我们仅考虑2个并发的协程都在执行该行代码。

前面的这行代码在计算机内部其实是拆成了如下几个步骤执行的(真实情况会更加复杂，这里为了说明问题简化为3步)。

1. 获取`x`变量当前值
2. 计算`x + 1`
3. 将计算结果赋值给变量`x`

如果这3步仅在一个协程内执行，那么不会有什么逻辑问题，但是一旦涉及到并发编程的情况就会更加复杂。现在我们看下如果有2个并发的协程在同时执行前面的这3步会发生什么情况。

![](https://1527815414-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-L_XyF-PcxCv1bX8ii73%2F-Lh17l5KET0ukY-fzL2W%2F-Lh18Kep4eEPKHaE55cO%2F5.png?alt=media\&token=55287422-c59a-4bbe-8767-acdc9994d3ad)

1. 我们假设变量`x`的初始值为0。
2. 系统先调度协程1，获取`x`的值，并计算出`x + 1`。
3. 系统再调度协程2，同样的获取`x`的值，并计算`x + 1`，但是需要注意，此刻获取到的`x`仍然是0。
4. 系统再切换到协程1，将计算结果1赋值给变量`x`，现在`x`变为1。
5. 系统再次切换到协程2，将协程2内部的计算结果1赋值给变量`x`，这时`x`仍然是1。
6. 最终虽然每个协程都调用了`x = x + 1`，但是变量`x`的值为1。

这中调度只是其中一种可能的方式，下面我们再看另外一种可能的调度顺序。

![](https://1527815414-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-L_XyF-PcxCv1bX8ii73%2F-Lh17l5KET0ukY-fzL2W%2F-Lh18XdL1RbG0sMqFaES%2F6.png?alt=media\&token=6017705a-4e86-454e-9a8f-aaf200abfff2)

1. 我们假设变量`x`的初始值为0。
2. 系统先调度协程1，获取`x`的值，并计算出`x + 1`，将计算结果1赋值给变量`x`，此时变量`x`值为1。
3. 系统再调度协程2，获取`x`的值，这时获取到的是1，并计算`x + 1`，计算结果为2，接着将计算结果2赋值给变量`x`，此时变量`x`值为2。
4. 最终变量`x`的值为2。

这两种不同的执行顺序都有可能出现，因此程序输出结果也就不稳定，会引起bug。我们一般称这种情况为`竞态`。

存在竞态的根本原因是临界区的代码被并发的协程同时访问。因此如果我们能够控制临界区的代码每次仅能被一个协程执行，就能避免竞态的发生。借助本节将要介绍的`互斥锁`便可以解决竞态问题。

#### 互斥锁

互斥锁用来保证临界区的代码每次仅能被一个协程执行，以防止竞态问题发生。

Golang中互斥锁`Mutex`位于`sync`包中，`Mutex`上定义了两个方法，`Lock`和`Unlock`。所有位于`Lock`和`Unlock`之间的代码每次仅能被一个协程执行，即有效避免了竞态问题。

```go
mutex.Lock()
x = x + 1
mutex.Unlock()
```

这里`x = x + 1`每次仅能被一个协程执行，有效避免了竞态问题。

**如果某个协程已经先获取到了锁，另外的协程尝试获取该锁时会阻塞，知道锁被释放。**

#### 竞态问题举例

这里我们先来看一个存在竞态问题的例子，然后通过互斥锁来解决该竞态问题。

```go
package main

import (
    "fmt"
    "sync"
)

var x = 0

func increment(wg *sync.WaitGroup) {
    x = x + 1
    wg.Done()
}
func main() {
    var w sync.WaitGroup
    for i := 0; i < 1000; i++ {
        w.Add(1)
        go increment(&w)
    }
    w.Wait()
    fmt.Println("final value of x", x)
}
```

这里通过函数`increment`来创建协程，我们一共创建了1000个并发协程，在每个协程内部执行`x = x + 1`，显然这里的`x`是个共享资源，存在竞态问题。该程序执行时的输出是不稳定的，比如我本机的输出为：

```
final value of x 919
```

由于竞态问题的存在，因此执行结果不是1000。

#### 使用互斥锁解决竞态问题

前面的程序中我们创建了1000个协程，在每个协程内将变量`x`递增1，因此我们预期的是输出1000。下面用互斥锁来保证逻辑的正确：

```go
package main

import (
    "fmt"
    "sync"
)

var x = 0

func increment(wg *sync.WaitGroup, m *sync.Mutex) {
    m.Lock()
    x = x + 1
    m.Unlock()
    wg.Done()
}
func main() {
    var w sync.WaitGroup
    var m sync.Mutex
    for i := 0; i < 1000; i++ {
        w.Add(1)
        go increment(&w, &m)
    }
    w.Wait()
    fmt.Println("final value of x", x)
}
```

`Mutex`是个结构体类型，这里我们声明了一个互斥锁类型变量`m`。然后在临界区代码`x = x + 1`前后加了`m.Lock()`以及`m.Unlock()`两行代码，即加上了锁，这样便避免了多个协程访问共享资源的竞态问题。该程序输出如下：

```
final value of x 1000
```

**这里需要注意我们是怎么使用互斥锁的。我们创建了一个互斥锁变量，然后将指针传给了所有协程。这样才能让所有协程去争抢一把锁，否则如果是值传递，所有协程都有一份独立的拷贝出来的锁，竞态问题依然会发生。**

#### 使用管道解决竞态问题

除了互斥锁之外，我们还可以通过管道来解决前面的竞态问题。

```go
package main

import (
    "fmt"
    "sync"
)

var x = 0

func increment(wg *sync.WaitGroup, ch chan bool) {
    ch <- true
    x = x + 1
    <-ch
    wg.Done()
}
func main() {
    var w sync.WaitGroup
    ch := make(chan bool, 1)
    for i := 0; i < 1000; i++ {
        w.Add(1)
        go increment(&w, ch)
    }
    w.Wait()
    fmt.Println("final value of x", x)
}
```

这里我们创建了一个带缓存的管道，容量为1。我们在临界区代码`x = x + 1`之前增加了一行代码`ch <- true`，该行代码写了1个数据到管道中，由于管道容量为1，因此其他协程如果也想写数据到该管道中，就会阻塞，直到管道中的这1个数据被读走。而只有临界区的代码执行完，我们才通过`<-ch`读走了管道中的数据，因此也就避免了竞态问题。该程序输出为：

```
final value of x 1000
```

#### 互斥锁 vs 管道

前面互斥锁和管道两种方案都可以解决竞态问题，那么怎么判断应该用哪个方案呢？这还得看具体的问题场景是什么样的。如果需要通过管道来传递一些有意义的数据，那就最好使用管道的方案，如果仅仅是为了解决竞态问题，比如前面的例子，那就最好使用互斥锁。既然Golang提供了这几种不同的工具，我们就要对症下药，选择最合适的解决方案。
