24

Go语言中的互斥锁和读写锁(Mutex和RWMutex)

 3 years ago
source link: http://www.cnblogs.com/chenqionghe/p/13919427.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

目录

虽然Go语言提供channel来保证协程的通信,但是某些场景用锁来显示保证协程的安全更清晰易懂。

Go语言中主要有两种锁,互斥锁Mutex和读写锁RWMutex,下面分别介绍一下使用方法,以及出现死锁的常见场景。

一、Mutex(互斥锁)

Mutex是互斥锁的意思,也叫排他锁,同一时刻一段代码只能被一个线程运行,使用只需要关注方法Lock(加锁)和Unlock(解锁)即可。

在Lock()和Unlock()之间的代码段称为资源的临界区(critical section),是线程安全的,任何一个时间点都只能有一个goroutine执行这段区间的代码。

不加锁示例

先来一段不加群的代码,10个协程同时累加1万

package main

import (
    "fmt"
    "sync"
)

func main() {
    var count = 0
    var wg sync.WaitGroup
    //十个协程数量
    n := 10
    wg.Add(n)
    for i := 0; i < n; i++ {
        go func() {
            defer wg.Done()
            //1万叠加
            for j := 0; j < 10000; j++ {
                count++
            }
        }()
    }
    wg.Wait()
    fmt.Println(count)
}

运行结果如下

正确的结果应该是100000,这里出现了并发写入更新错误的情况

加锁示例

我们再添加锁,代码如下

package main

import (
    "fmt"
    "sync"
)

func main() {
    var count = 0
    var wg sync.WaitGroup
    var mu sync.Mutex
    //十个协程数量
    n := 10
    wg.Add(n)
    for i := 0; i < n; i++ {
        go func() {
            defer wg.Done()
            //1万叠加
            for j := 0; j < 10000; j++ {
                mu.Lock()
                count++
                mu.Unlock()
            }
        }()
    }
    wg.Wait()
    fmt.Println(count)
}

运行结果如下,可以看到,已经看到结果变成了正确的100000

IfYvi2u.png!mobile

二、RWMutex(读写锁)

Mutex在大量并发的情况下,会造成锁等待,对性能的影响比较大。

如果某个读操作的协程加了锁,其他的协程没必要处于等待状态,可以并发地访问共享变量,这样能让读操作并行,提高读性能。

RWLock就是用来干这个的,这种锁在某一时刻能由什么问题数量的reader持有,或者被一个wrtier持有

主要遵循以下规则 :

  1. 读写锁的读锁可以重入,在已经有读锁的情况下,可以任意加读锁。
  2. 在读锁没有全部解锁的情况下,写操作会阻塞直到所有读锁解锁。
  3. 写锁定的情况下,其他协程的读写都会被阻塞,直到写锁解锁。

Go语言的读写锁方法主要有下面这种

  1. Lock/Unlock:针对写操作。
    不管锁是被reader还是writer持有,这个Lock方法会一直阻塞,Unlock用来释放锁的方法
  2. RLock/RUnlock:针对读操作
    当锁被reader所有的时候,RLock会直接返回,当锁已经被writer所有,RLock会一直阻塞,直到能获取锁,否则就直接返回,RUnlock用来释放锁的方法

并发读示例

package main

import (
    "fmt"
    "sync"
    "time"
)

func main() {
    var m sync.RWMutex
    go read(&m, 1)
    go read(&m, 2)
    go read(&m, 3)

    time.Sleep(2 * time.Second)
}

func read(m *sync.RWMutex, i int) {
    fmt.Println(i, "reader start")
    m.RLock()
    fmt.Println(i, "reading")
    time.Sleep(1 * time.Second)
    m.RUnlock()

    fmt.Println(i, "reader over")
}

运行如下

vIVfeiZ.png!mobile

可以看到,3的读还没结束,1和2已经开始读了

并发读写示例

package main

import (
    "fmt"
    "sync"
    "time"
)

var count = 0

func main() {
    var m sync.RWMutex
    for i := 1; i <= 3; i++ {
        go write(&m, i)
    }
    for i := 1; i <= 3; i++ {
        go read(&m, i)
    }

    time.Sleep(1 * time.Second)
    fmt.Println("final count:", count)
}

func read(m *sync.RWMutex, i int) {
    fmt.Println(i, "reader start")
    m.RLock()
    fmt.Println(i, "reading count:", count)
    time.Sleep(1 * time.Millisecond)
    m.RUnlock()

    fmt.Println(i, "reader over")
}

func write(m *sync.RWMutex, i int) {
    fmt.Println(i, "writer start")
    m.Lock()
    count++
    fmt.Println(i, "writing count", count)
    time.Sleep(1 * time.Millisecond)
    m.Unlock()

    fmt.Println(i, "writer over")
}

运行结果如下

YJb2ym2.png!mobile

如果我们可以明确区分reader和writer的协程场景,且是大师的并发读、少量的并发写,有强烈的性能需要,我们就可以考虑使用读写锁RWMutex替换Mutex

三、死锁场景

当两个或两个以上的进程在执行过程中,因争夺资源而处理一种互相等待的状态,如果没有外部干涉无法继续下去,这时我们称系统处于死锁或产生了死锁。

死锁主要有以下几种场景。

Lock/Unlock不是成对出现

没有成对出现容易会出现死锁的情况,或者是Unlock 一个未加锁的Mutex而导致 panic,代码建议以下面紧凑的方式出现

mu.Lock()
defer mu.Unlock()

锁被拷贝使用

package main

import (
    "fmt"
    "sync"
)

func main() {
    var mu sync.Mutex
    mu.Lock()
    defer mu.Unlock()
    copyTest(mu)
}

//这里复制了一个锁,造成了死锁
func copyTest(mu sync.Mutex) {
    mu.Lock()
    defer mu.Unlock()
    fmt.Println("ok")
}

在函数外层已经加了一个Lock,在拷贝的时候又执行了一次Lock,因此这是一个永远不会获得的锁,因为外层函数的Unlock无法执行。

循环等待

A等待B,B等待C,C等待A,陷入了无限循环(哲学家就餐问题)

package main

import (
    "sync"
)

func main() {
    var muA, muB sync.Mutex
    var wg sync.WaitGroup

    wg.Add(2)
    go func() {
        defer wg.Done()
        muA.Lock()
        defer muA.Unlock()
        //A依赖B
        muB.Lock()
        defer muB.Lock()
    }()

    go func() {
        defer wg.Done()
        muB.Lock()
        defer muB.Lock()
        //B依赖A
        muA.Lock()
        defer muA.Unlock()
    }()
    wg.Wait()
}

以上就是Go语言的锁使用,由chenqionghe倾情整理,giao~


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK