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 增加后,信道的值才会被读取。实际上这就保证了只允许一个协程访问临界区。

相关推荐
songtaiwu8 小时前
golang开发日常注意
开发语言·数据库·golang
王中阳Go12 小时前
Go后端 vs Go AI应用开发重点关注什么?怎么学?怎么面试?
人工智能·面试·golang
源代码•宸13 小时前
GoLang并发示例代码1(关于逻辑处理器运行顺序)
开发语言·经验分享·后端·golang
卿雪13 小时前
MySQL【存储引擎】:InnoDB、MyISAM、Memory...
java·数据库·python·sql·mysql·golang
路边草随风13 小时前
go实现接口接收数据写入kafka
golang·kafka·gin
qq_5486749314 小时前
Go的env和bashrc的GOROOT配置不同
linux·开发语言·golang
Zfox_14 小时前
【Go】反射
开发语言·后端·golang
Rinai_R14 小时前
Golang 垃圾回收器执行链路分析
开发语言·后端·golang
古城小栈14 小时前
深入解析Go泛型中的~struct{}
开发语言·后端·golang