Golang 的同步锁与读写锁

同步锁

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

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

  • 使用Lock()加锁后,不能再继续对其加锁(同一个goroutine中,即:同步调用),否则会panic。只有在unlock()之后才能再次Lock()。异步调用Lock(),是正当的锁竞争,当然不会有panic了。适用于读写不确定场景,即读写次数没有明显的区别,并且只允许只有一个读或者写的场景,所以该锁也叫做全局锁。

  • func (m *Mutex) Unlock()用于解锁m,如果在使用Unlock()前未加锁,就会引起一个运行错误。已经锁定的Mutex并不与特定的goroutine相关联,这样可以利用一个goroutine对其加锁,再利用其他goroutine对其解锁。

建议:同一个互斥锁的成对锁定和解锁操作放在同一层次的代码块中。\
使用锁的经典模式:

var lck sync.Mutex
func foo() {
    lck.Lock() 
    defer lck.Unlock()
    // ...
}

lck.Lock()会阻塞直到获取锁,然后利用defer语句在函数返回时自动释放锁。

下面代码通过3个goroutine来体现sync.Mutex 对资源的访问控制特征:

package main
import (
    "fmt"
    "sync"
    "time"
)
func main() {
    wg := sync.WaitGroup{}
    var mutex sync.Mutex
    fmt.Println("Locking  (G0)")
    mutex.Lock()
    fmt.Println("locked (G0)")
    wg.Add(3)
    for i := 1; i < 4; i++ {
        go func(i int) {
            fmt.Printf("Locking (G%d)\n", i)
            mutex.Lock()
            fmt.Printf("locked (G%d)\n", i)
            time.Sleep(time.Second * 2)
            mutex.Unlock()
            fmt.Printf("unlocked (G%d)\n", i)
            wg.Done()
        }(i)
    }
    time.Sleep(time.Second * 5)
    fmt.Println("ready unlock (G0)")
    mutex.Unlock()
    fmt.Println("unlocked (G0)")
    wg.Wait()
}
程序输出:
Locking  (G0)
locked (G0)
Locking (G1)
Locking (G3)
Locking (G2)
ready unlock (G0)
unlocked (G0)
locked (G1)
unlocked (G1)
locked (G3)
locked (G2)
unlocked (G3)
unlocked (G2)

通过程序执行结果我们可以看到,当有锁释放时,才能进行lock动作,G0锁释放时,才有后续锁释放的可能,这里是G1抢到释放机会。

Mutex也可以作为struct的一部分,这样这个struct就会防止被多线程更改数据。

package main
import (
    "fmt"
    "sync"
    "time"
)
type Book struct {
    BookName string
    L        *sync.Mutex
}
func (bk *Book) SetName(wg *sync.WaitGroup, name string) {
    defer func() {
        fmt.Println("Unlock set name:", name)
        bk.L.Unlock()
        wg.Done()
    }()
    bk.L.Lock()
    fmt.Println("Lock set name:", name)
    time.Sleep(1 * time.Second)
    bk.BookName = name
}
func main() {
    bk := Book{}
    bk.L = new(sync.Mutex)
    wg := &sync.WaitGroup{}
    books := []string{"《三国演义》", "《道德经》", "《西游记》"}
    for _, book := range books {
        wg.Add(1)
        go bk.SetName(wg, book)
    }
    wg.Wait()
}

程序输出:
Lock set name: 《西游记》
Unlock set name: 《西游记》
Lock set name: 《三国演义》
Unlock set name: 《三国演义》
Lock set name: 《道德经》
Unlock set name: 《道德经》

读写锁

读写锁是分别针对读操作和写操作进行锁定和解锁操作的互斥锁。在Go语言中,读写锁由结构体类型sync.RWMutex代表。

基本遵循原则:

写锁定情况下,对读写锁进行读锁定或者写锁定,都将阻塞;而且读锁与写锁之间是互斥的;

读锁定情况下,对读写锁进行写锁定,将阻塞;加读锁时不会阻塞;

对未被写锁定的读写锁进行写解锁,会引发Panic;

对未被读锁定的读写锁进行读解锁的时候也会引发Panic;

写解锁在进行的同时会试图唤醒所有因进行读锁定而被阻塞的goroutine;

读解锁在进行的时候则会试图唤醒一个因进行写锁定而被阻塞的goroutine。

与互斥锁类似,sync.RWMutex类型的零值就已经是立即可用的读写锁了。在此类型的方法集合中包含了两对方法,即:

RWMutex提供四个方法:

func (*RWMutex) Lock // 写锁定
func (*RWMutex) Unlock // 写解锁
func (*RWMutex) RLock // 读锁定
func (*RWMutex) RUnlock // 读解锁
package main
import (
    "fmt"
    "sync"
    "time"
)
var m *sync.RWMutex
func main() {
    wg := sync.WaitGroup{}
    wg.Add(20)
    var rwMutex sync.RWMutex
    Data := 0
    for i := 0; i < 10; i++ {
        go func(t int) {
            rwMutex.RLock()
            defer rwMutex.RUnlock()
            fmt.Printf("Read data: %v\n", Data)
            wg.Done()
            time.Sleep(2 * time.Second)
            // 这句代码第一次运行后,读解锁。
            // 循环到第二个时,读锁定后,这个goroutine就没有阻塞,同时读成功。
        }(i)
        go func(t int) {
            rwMutex.Lock()
            defer rwMutex.Unlock()
            Data += t
            fmt.Printf("Write Data: %v %d \n", Data, t)
            wg.Done() 
            // 这句代码让写锁的效果显示出来,写锁定下是需要解锁后才能写的。
            time.Sleep(2 * time.Second)        
        }(i)
    }
    time.Sleep(5 * time.Second)
    wg.Wait()
}
mushu
讨论数量: 2

Locking (G0) locked (G0) Locking (G1) Locking (G3) Locking (G2) ready unlock (G0) unlocked (G0) locked (G1) unlocked (G1) locked (G3) locked (G2) unlocked (G3) unlocked (G2)

这里互斥锁的输出不对吧,倒数第3和4 不可能同时解锁

7个月前 评论
4fun 4个月前

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!