Go 语言包中的 sync 包提供了两种锁类型:sync.Mutex 和 sync.RWMutex,前者是互斥锁,后者是读写锁
互斥锁
互斥锁是传统的并发程序对共享资源进行访问控制的主要手段,在 Go 中,似乎更推崇由 channel 来实现资源共享和通信。它由标准库代码包 sync 中的 Mutex 结构体类型代表。只有两个公开方法:调用 Lock()获得锁,调用 unlock()释放锁。
建议:同一个互斥锁的成对锁定和解锁操作放在同一层次的代码块中。\
使用锁的经典模式:
var lck sync.Mutex
func foo() {
lck.Lock()
defer lck.Unlock()
// ...
}
示例
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。
总结来说:读读不互斥,读写互斥,写写互斥。
————————————————
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()
}
Read data: 0
Write Data: 2 2
Read data: 2
Read data: 2
Read data: 2
Read data: 2
Read data: 2
Read data: 2
Read data: 2
Read data: 2
Read data: 2
Write Data: 2 0
Write Data: 3 1
Write Data: 8 5
Write Data: 11 3
Write Data: 18 7
Write Data: 24 6
Write Data: 32 8
Write Data: 41 9
Write Data: 45 4