go(基础04)——mutex

1. 临界区

当程序并发地运行时,多个 Go 协程不应该同时访问那些修改共享资源的代码。这些修改共享资源的代码称为临界区。例如,假设我们有一段代码,将一个变量 x 自增 1。

Go 复制代码
        x = x + 1

如果只有一个 Go 协程访问上面的代码段,那都没有任何问题。

在上一行代码的内部,系统执行程序时分为如下几个步骤:

1)获得 x 的当前值

2)计算 x + 1

3)将步骤 2 计算得到的值赋值给 x

如果只有一个协程执行上面的三个步骤,不会有问题。

我们讨论一下当有两个并发的协程执行该代码时,会发生什么。下图描述了当两个协程并发地访问代码行 x = x + 1 时,可能出现的一种情况。

我们假设 x 的初始值为 0。而协程 1 获取 x 的初始值,并计算 x + 1。而在协程 1 将计算值赋值给 x 之前,系统上下文切换到了协程 2。于是,协程 2 获取了 x 的初始值(依然为 0),并计算 x + 1。接着系统上下文又切换回了协程 1。现在,协程 1 将计算值 1 赋值给 x,因此x 等于 1。然后,协程 2 继续开始执行,把计算值(依然是 1)复制给了 x,因此在所有协程执行完毕之后,x 都等于 1。

现在我们考虑另外一种可能发生的情况。

在上面的情形里,协程 1 开始执行,完成了三个步骤后结束,因此 x 的值等于 1。接着,开始执行协程 2。目前 x 的值等于 1。而当协程 2 执行完毕时,x 的值等于 2。

所以,从这两个例子你可以发现,根据上下文切换的不同情形,x 的最终值是 1 或者 2。这种不太理想的情况称为竞态条件(Race Condition),其程序的输出是由协程的执行顺序决定的。

在上例中,如果在任意时刻只允许一个 Go 协程访问临界区,那么就可以避免竞态条件。而使用 Mutex 可以达到这个目的。

2. Mutex

Mutex 用于提供一种加锁机制(Locking Mechanism),可确保在某时刻只有一个协程在临界区运行,以防止出现竞态条件。

Mutex 可以在 sync包内找到。Mutex定义了两个方法:Lock和 Unlock。所有在Lock 和 Unlock 之间的代码,都只能由一个 Go 协程执行,于是就可以避免竞态条件。

Go 复制代码
        mutex.Lock()
        x = x + 1
        mutex.Unlock()

在上面的代码中,x = x + 1 只能由一个 Go 协程执行,因此避免了竞态条件。

如果有一个 Go 协程已经持有了锁(Lock),当其他协程试图获得该锁时,这些协程会被阻塞,直到 Mutex 解除锁定为止。

3. 含有竞态条件的程序

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)
}

执行结果(每次执行都会产生不同的结果):

4. 使用Mutex

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)
}

执行结果(每次固定是100):

5. 使用信道处理竞态条件

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 的缓冲信道,并将它传入 increment 协程。该缓冲信道用于保证只有一个协程访问增加 x 的临界区。具体的实现方法是在 x 增加之前,传入 true 给缓冲信道。由于缓冲信道的容量为 1,所以任何其他协程试图写入该信道时,都会发生阻塞,直到 x 增加后,信道的值才会被读取。实际上这就保证了只允许一个协程访问临界区。

相关推荐
GDAL15 分钟前
Go Channel `close()` 深入全面讲解
golang·通道·close
Tomhex2 小时前
Golang内置函数总结
golang·go
XMYX-02 小时前
05 - Go 的循环与判断:语法、用法与最佳实践
开发语言·golang
被摘下的星星4 小时前
Go赋值操作的关键细节
开发语言·golang
喵了几个咪4 小时前
Go 语言 CMS 横评:风行 GoWind 对比传统 PHP/Java CMS 核心优势
java·golang·php
喵了几个咪4 小时前
Headless 架构优势:内容与展示解耦,一套 API 打通全端生态
vue.js·架构·golang·cms·react·taro·headless
Wenweno0o1 天前
0基础Go语言Eino框架智能体实战-chatModel
开发语言·后端·golang
咬_咬1 天前
go语言学习(基本数据类型)
开发语言·学习·golang·数据类型
搜佛说1 天前
01-第1章-概述与快速开始
物联网·golang·开源·软件工程·边缘计算·嵌入式实时数据库
LlNingyu1 天前
什么是Go的接口(二)
golang