Go语言中 sync 包里提供了互斥锁 Mutex 和读写锁 RWMutex 用于处理并发过程中可能出现同时两个或多个协程(或线程)读或写同一个变量的情况。
锁是 sync 包中的核心,它主要有两个方法,分别是加锁(Lock)和解锁(Unlock)。
在并发的情况下,多个线程或协程同时其修改一个变量,使用锁能保证在某一时间内,只有一个协程或线程修改这一变量。
不使用锁时,在并发的情况下可能无法得到想要的结果,如下所示:
- package main
- import (
- "fmt"
- "time"
- )
- func main() {
- var a = 0
- for i := 0; i < 1000; i++ {
- go func(idx int) {
- a += 1
- fmt.Println(a)
- }(i)
- }
- time.Sleep(time.Second)
- }
从理论上来说,上面的程序会将 a 的值依次递增输出,然而实际结果却是下面这样子的。
通过运行结果可以看出 a 的值并不是按顺序递增输出的,这是为什么呢?协程的执行顺序大致如下所示:
按照上面的顺序,假如有一个协程取得 a 的值为 3,然后执行加法运算,此时又有一个协程对 a 进行取值,得到的值同样是 3,最终两个协程的返回结果是相同的。
而锁的概念就是,当一个协程正在处理 a 时将 a 锁定,其它协程需要等待该协程处理完成并将 a 解锁后才能再进行操作,也就是说同时处理 a 的协程只能有一个,从而避免上面示例中的情况出现。
上面的示例中出现的问题怎么解决呢?加一个互斥锁 Mutex 就可以了。那什么是互斥锁呢 ?互斥锁中其有两个方法可以调用,如下所示:
将上面的代码略作修改,如下所示:
- package main
-
- import (
- "fmt"
- "sync"
- "time"
- )
-
- func main() {
- var a = 0
- var lock sync.Mutex
- for i := 0; i < 1000; i++ {
- go func(idx int) {
- lock.Lock()
- defer lock.Unlock()
- a += 1
- fmt.Printf("goroutine %d, a=%d\n", idx, a)
- }(i)
- }
- // 等待 1s 结束主程序
- // 确保所有协程执行完
- time.Sleep(time.Second)
- }
运行结果如下:
需要注意的是一个互斥锁只能同时被一个 goroutine 锁定,其它 goroutine 将阻塞直到互斥锁被解锁(重新争抢对互斥锁的锁定),示例代码如下:
- package main
- import (
- "fmt"
- "sync"
- "time"
- )
- func main() {
- ch := make(chan struct{}, 2)
- var l sync.Mutex
- go func() {
- l.Lock()
- defer l.Unlock()
- fmt.Println("goroutine1: 我会锁定大概 2s")
- time.Sleep(time.Second * 2)
- fmt.Println("goroutine1: 我解锁了,你们去抢吧")
- ch <- struct{}{}
- }()
- go func() {
- fmt.Println("goroutine2: 等待解锁")
- l.Lock()
- defer l.Unlock()
- fmt.Println("goroutine2: 欧耶,我也解锁了")
- ch <- struct{}{}
- }()
- // 等待 goroutine 执行结束
- for i := 0; i < 2; i++ {
- <-ch
- }
- }
上面的代码运行结果如下:
读写锁有如下四个方法:
读写锁的区别在于:
所以说这里的读锁定(RLock)目的其实是告诉写锁定,有很多协程或者进程正在读取数据,写操作需要等它们读(读解锁)完才能进行写(写锁定)。
我们可以将其总结为如下三条:
示例代码如下所示:
- package main
- import (
- "fmt"
- "math/rand"
- "sync"
- )
- var count int
- var rw sync.RWMutex
- func main() {
- ch := make(chan struct{}, 10)
- for i := 0; i < 5; i++ {
- go read(i, ch)
- }
- for i := 0; i < 5; i++ {
- go write(i, ch)
- }
- for i := 0; i < 10; i++ {
- <-ch
- }
- }
- func read(n int, ch chan struct{}) {
- rw.RLock()
- fmt.Printf("goroutine %d 进入读操作...\n", n)
- v := count
- fmt.Printf("goroutine %d 读取结束,值为:%d\n", n, v)
- rw.RUnlock()
- ch <- struct{}{}
- }
- func write(n int, ch chan struct{}) {
- rw.Lock()
- fmt.Printf("goroutine %d 进入写操作...\n", n)
- v := rand.Intn(1000)
- count = v
- fmt.Printf("goroutine %d 写入结束,新值为:%d\n", n, v)
- rw.Unlock()
- ch <- struct{}{}
- }
其执行结果如下:
下面再来看两个示例。
【示例 1】多个读操作同时读取一个变量时,虽然加了锁,但是读操作是不受影响的。(读和写是互斥的,读和读不互斥)
- package main
- import (
- "sync"
- "time"
- )
- var m *sync.RWMutex
- func main() {
- m = new(sync.RWMutex)
- // 多个同时读
- go read(1)
- go read(2)
- time.Sleep(2*time.Second)
- }
- func read(i int) {
- println(i,"read start")
- m.RLock()
- println(i,"reading")
- time.Sleep(1*time.Second)
- m.RUnlock()
- println(i,"read over")
- }
运行结果如下:
【示例 2】由于读写互斥,所以写操作开始的时候,读操作必须要等写操作进行完才能继续,不然读操作只能继续等待。
- package main
- import (
- "sync"
- "time"
- )
- var m *sync.RWMutex
- func main() {
- m = new(sync.RWMutex)
- // 写的时候啥也不能干
- go write(1)
- go read(2)
- go write(3)
- time.Sleep(2*time.Second)
- }
- func read(i int) {
- println(i,"read start")
- m.RLock()
- println(i,"reading")
- time.Sleep(1*time.Second)
- m.RUnlock()
- println(i,"read over")
- }
- func write(i int) {
- println(i,"write start")
- m.Lock()
- println(i,"writing")
- time.Sleep(1*time.Second)
- m.Unlock()
- println(i,"write over")
- }
运行结果如下: