go中锁的类型

发布于 2021-12-27  240 次阅读


Go 语言包中的 sync 包提供了两种锁类型:sync.Mutex 和 sync.RWMutex,前者是互斥锁,后者是读写锁。

互斥锁是传统的并发程序对共享资源进行访问控制的主要手段,在 Go 中,似乎更推崇由 channel 来实现资源共享和通信。它由标准库代码包 sync 中的 Mutex 结构体类型代表。只有两个公开方法:调用 Lock()获得锁,调用 unlock()释放锁。

go🔒有多少种

先放出源码的位置

go/mutex.go at master · golang/go (github.com)

go/rwmutex.go at master · golang/go (github.com)

sync.Mutex

在源码中的样子

image-20211029213113596

image-20211029213036738

其中Mutex.state表示了当前互斥锁处于的状态

waiterNum 表示目前互斥锁等待队列中有多少goroutine在等待

straving 表示目前互斥锁是否处于饥饿状态

woken 表示目前互斥锁是否为唤醒状态

locked 表示目前互斥锁资源是否被goroutine持有

Mutex.sema主要用于等待队列

互斥锁的状态

互斥锁通常保持两种状态 正常模式饥饿模式
引入饥饿模式的原因是,为了保持互斥锁的公平性。
正常模式下,锁资源一般会交给刚被唤醒的goroutine,而为了怕部分goroutine被“饿死”,所以引入了饥饿模式,在饥饿模式下,goroutine在释放锁资源的时候会将锁资源交给等待队列中的下一个goroutine。

怎么加锁、放锁

package main

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

// 不加锁并发写
func f1() {
    var count1 = 0
    var wg1 sync.WaitGroup
    //十个协程数量
    n1 := 10
    wg1.Add(n1)
    for i := 0; i < n1; i++ {
        go func() {
            defer wg1.Done()
            //1万叠加
            for j := 0; j < 10000; j++ {
                count1++
            }
        }()
    }
    wg1.Wait()
    fmt.Println("no mutex=>", count1)
}

// 加锁并发写
func f2() {
    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("has mutex=>", count)
}

func main() {
    // go f1()
    go f2()

    // 留出时间给协程完成任务
    time.Sleep(10 * time.Second)
    fmt.Println("main quit")
}
  • 执行结果

image-20211029214203295

  • 发现如果,不加mutex,会导致部分写入更新失败。

sync.RWMutex

源码中的样子(删减版)

type RWMutex struct {
    w           Mutex  // held if there are pending writers
    writerSem   uint32 // semaphore for writers to wait for completing readers
    readerSem   uint32 // semaphore for readers to wait for completing writers
    readerCount int32  // number of pending readers
    readerWait  int32  // number of departing readers
}

const rwmutexMaxReaders = 1 << 30

func (rw *RWMutex) RLock() {
}

func (rw *RWMutex) RUnlock() {
}

func (rw *RWMutex) rUnlockSlow(r int32) {
}

func (rw *RWMutex) Lock() {
}

func (rw *RWMutex) Unlock() {
}

func (rw *RWMutex) RLocker() Locker {
    return (*rlocker)(rw)
}

type rlocker RWMutex

func (r *rlocker) Lock()   { (*RWMutex)(r).RLock() }
func (r *rlocker) Unlock() { (*RWMutex)(r).RUnlock() }

::: note

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)
    fmt.Println("main quit")
}

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

image-20211029214945505

并发读写

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")
}
  • 执行结果

image-20211029215500517

参考

  1. Go互斥锁源码解析 - 知乎 (zhihu.com)
  2. Go语言中的互斥锁和读写锁(Mutex和RWMutex) - 雪山飞猪 - 博客园 (cnblogs.com)
  3. [Golang 的同步锁与读写锁 | Go 技术论坛 (learnku.com)](https://learnku.com/go/t/32513#:~:text=Go 语言包中的 sync 包提供了两种锁类型:sync.Mutex 和,sync.RWMutex,前者是互斥锁,后者是读写锁。 互斥锁是传统的并发程序对共享资源进行访问控制的主要手段,在 Go 中,似乎更推崇由 channel 来实现资源共享和通信。)

见其广知其深