美文网首页
Go教程第二十二篇:Mutex

Go教程第二十二篇:Mutex

作者: 大风过岗 | 来源:发表于2020-05-16 11:07 被阅读0次

    本文是《Go系列教程》的第二十二篇文章。

    在这篇文章中,我们要讲解关于mutex的内容。我们还会学习到如何使用mutex和channel解决资源竞争问题。

    临界区

    在讲临界区之前,我们必须理解临界区的概念在并发编程中,当一个程序并发运行时,不能有多个Goroutine在同一时间访问修改共享资源的代码段。此时的代码段就是临界区。
    例如,假定我们有一段代码会使变量x自增1。

    x = x + 1
    
    

    只要上面的这段代码仅仅被一个Goroutine访问,那么就不会有任何问题。但我们需要知道,为什么当有多个Goroutine并发运行这段代码的时候,就会出问题呢。
    为了简化起见,我们假定有2个Goroutine并发运行上面的代码。这段代码在操作系统内部的执行步骤如下,

    • 1、获取x的当前值

    • 2、计算X+1

    • 3、把计算后的值赋值给X

    当以上这3个步骤只有一个Goroutine,所有的都会运行良好。

    我们讨论一下,当有2个Goroutine并发运行这段代码的时候,到底发生了什么呢。下面这个图展示了当有2个Goroutine并发访问x=x+1代码时的内部逻辑。

    图1

    我们假定x的初始值为0。Goroutine 1先获取x的初始值,计算X+1,在把计算后的值赋值给X之前,系统上下文切换到Goroutine2。现在Goroutine2也获取了x的初始值,x的初始值还是0。
    这是Goroutine2继续计算x+1。在系统上下文再次切换到Goroutine 1之后,现在Goroutine 1把计算后的值1赋值给x。因此,x就变成了1。之后,Goroutine 2再次启动运行,之后再次把
    计算后的值赋值给x,即:又把1赋值给了x。因此,在这俩个Goroutine执行之后,x就变成了1。

    现在,我们再来看一个不同的应用场景。

    图2

    在上面的场景中,Goroutine1启动执行,并执行完所有的这3个步骤,因此,x的值变成了1。之后,Goroutine 2开始执行。现在x的值是1,当Goroutine 2运行结束时,x的值就是2。

    因此,从上面这俩种场景,你可以看到x的最终值,有可能是1也有可能是2,它完全取决于系统的上下文是如何切换的。像这种情况,就称为 资源竞争。

    在上面的程序中,如果在任何时间点,只允许一个Goroutine访问这段临界区代码的话,资源竞争是完全可以避免的。我们通过使用Mutex就可以做到这一点。

    Mutex

    Mutex主要用于提供一个加锁机制来确保在任何时间点上,只有一个Goroutine运行这段临界区的代码。从而避免了资源竞争的发生。

    Mutex位于sync包中。在Mutex中定义了2个方法:Lock和Unlock。位于Lock和Unlock之间的代码只能被一个Goroutine执行,因此,避免了资源竞争。

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

    在上面这段代码,在任何一个时间点上,只有一个Goroutine能运行x=x+1。

    如果一个Goroutine已经持有了锁,另一个新的Goroutine试图去获取锁的话,这个新的Goroutine就会被阻塞住,直到持有锁的Goroutine释放了锁。

    发生资源竞争的程序

    在这段中,我们写一个有资源竞争的程序。在接下来的程序中,我们将解决这个资源竞争。

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

    在上面的程序中,第7行的increment函数会把x的值加1。之后调用WaitGroup的Done()函数来通知它完成。

    我们创建了1000个increment的Goroutine。这些Goroutine并发运行,当多个Goroutinet并发访问x的值,并试图增加x的值的时候,就会发生资源竞争。
    在你的本地机器上,多次运行此程序,你就可以看到在每一次输出的时候,内容都是不一样的,这就是资源竞争产生的。我这边运行时的输出有这几个:
    final value of x 941, final value of x 928, final value of x 922 等等。

    使用mutex解决资源竞争

    在上面的程序中,我们创建了1000个Goroutine。如果每一个Goroutine都把x的值加1的话,最终的值就是1000。在这部分,我们就使用mutex来解决资源竞争的问题。

    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是一个结构体类型,我们创建了一个零值的Mutex结构体类型的变量m。在上面的程序中,我们修改了increment函数,把x=x+1置于m.Lock和m.Unlock之间。
    现在这段代码就有效地避免了资源竞争,因为在任何时间点上,只允许有一个Goroutine可以运行这段代码。

    此时,如果运行程序的话,输出如下:

    final value of x 1000
    
    

    有一点非常重要的,那就是必须传入mutex的地址。如果传入的mutex不是地址而是值的话,每一个Goroutine将有一份自己的mutex副本,资源竞争依然会发生。

    使用channel解决资源竞争

    除了mutex之外,我们还可以使用channel来解决资源竞争问题。我们来看下它是怎么做到的。

    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的Goroutine。该缓冲区通道可用以确保只有一个Goroutine可以访问临界区代码。
    在x自增之前,把true传递给缓冲区通道。由于缓冲区通道的容量为1,所以,其他试图向此通道执行写入操作的Goroutine都会被阻塞住,直到x的增加完之后,
    可以从此通道中读取到数据为止。这样也可以有效地控制了只有一个Goroutine可以访问临界区代码。

    程序的输出如下:

    final value of x 1000
    

    Mutex VS Channel

    使用Mutex和Channel都可以解决资源竞争问题。那么我们在什么时候选择使用Mutex或Channel呢。答案取决于我们要解决的问题。如果我们要解决的问题更适合使用mutex解决的话,我们就使用
    mutex。如果要解决的问题,适合使用channel解决的话,那就使用channel。

    需要Go的初学者试图使用channel解决所有的并发问题,这是错误的。Go语言为我们提供了俩种解决并发问题的选项:Mutex和Channel。选择哪一个都行。

    通常情况下,当Goroutine需要彼此通信时,我们可以使用Channel。当只允许一个Goroutine访问临界区代码的时候,我们可以使用Mutex。

    在本例中,我更倾向于使用mutex来解决这个问题。因为该问题不需要Goroutine之间进行通信。因此,更适合使用mutex。

    我们的建议是,为问题选择解决方法,而不是为解决方法找问题。

    感谢您的阅读,请留下您珍贵的反馈和评论。Have a good Day!

    备注
    本文系翻译之作原文博客地址

    相关文章

      网友评论

          本文标题:Go教程第二十二篇:Mutex

          本文链接:https://www.haomeiwen.com/subject/etylohtx.html