Skip to content

Go语言互斥锁

Mutex(互斥锁)

Mutex是互斥锁的意思,也叫排他锁,同一时刻一段代码只能被一个线程运行,使用只需要关注方法Lock(加锁)和Unlock(解锁)即可。 在Lock()和Unlock()之间的代码段称为资源的临界区(critical section),是线程安全的,任何一个时间点都只能有一个goroutine执行这段区间的代码。

不加锁示例

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

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

运行结果如下

undefined
38532

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

加锁示例

我们再添加锁,代码如下

go
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

RWMutex(读写锁)

Mutex在大量并发的情况下,会造成锁等待,对性能的影响比较大。 如果某个读操作的协程加了锁,其他的协程没必要处于等待状态,可以并发地访问共享变量,这样能让读操作并行,提高读性能。 RWLock就是用来干这个的,这种锁在某一时刻能由什么问题数量的reader持有,或者被一个wrtier持有

主要遵循以下规则 :

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

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

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

并发读示例

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

运行如下

可以看到,3的读还没结束,1和2已经开始读了,提高了数据访问性能

并发读写示例

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

运行结果如下

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

死锁场景

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

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

go
mu.Lock()
defer mu.Unlock()

锁被拷贝使用

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

这段代码会造成死锁的原因在于 copyTest() 函数中对 sync.Mutex 类型的参数进行了值拷贝。在 Go 语言中,参数传递是按值传递的,这意味着函数参数在被调用时会创建一个副本,而不是直接引用原始数据。因此,在 copyTest() 函数中,mumain() 函数中创建的 sync.Mutex 的一个副本,它与原始的 mu 是不同的实例。

copyTest() 函数试图对副本的 mu 加锁时,实际上是对一个新的锁进行加锁操作,而在 main() 函数中,已经对原始的 mu 加锁了。这样就产生了两个不同的锁对象,在 copyTest() 函数中的 mu.Lock() 操作会被阻塞,因为它试图获取一个已经被 main() 函数持有的锁。而 main() 函数中的 defer mu.Unlock() 操作会一直等待,直到 copyTest() 函数中的 mu.Unlock() 执行完成,但由于 mu.Lock() 操作被阻塞了,mu.Unlock() 操作无法执行,导致了死锁。

循环等待

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

go
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()
}
  1. 主函数 main() 启动了两个 Goroutine,分别对 muAmuB 两个互斥锁进行了加锁操作。
  2. 第一个 Goroutine 先对 muA 加锁,然后试图对 muB 加锁,而第二个 Goroutine 先对 muB 加锁,然后试图对 muA 加锁。
  3. 由于两个 Goroutine 都试图获取对方已经持有的锁,因此它们都会被阻塞,无法继续执行。
  4. 这样就产生了死锁:两个 Goroutine 互相等待对方释放锁,但又无法释放自己所持有的锁,导致程序无法继续执行。