首页 > 编程笔记 > Go语言笔记 阅读:6

Go语言锁机制详解(sync.Mutex和sync.RWMutex)

Go 语言的锁机制是为了使多个并发之间能按照一定的顺序执行,加锁后的程序会一直占用数据和资源,直到解锁为止。

锁机制是由内置包 sync 实现的,锁类型分别为 sync.Mutex 和 sync.RWMutex,两者说明如下:

1) sync.Mutex 是互斥锁,仅支持一个 Goroutine(并发程序)对数据进行读写操作。当一个 Goroutine 获得 Mutex 锁之后,其他 Goroutine 只能等待该 Goroutine 释放锁,否则将一直处于阻塞等待状态。

2) sync.RWMutex 是读写互斥锁,它仅允许一个 Goroutine对数据执行写入操作,但支持多个 Goroutine 同时读取数据,数据读取和写入分别由不同方法实现。如果从底层原理分析,sync.RWMutex 是在 sync.Mutex 的基础上进行功能扩展,使其支持数据多读模式。

我们打开 sync.Mutex 源码看到,它以结构体方式定义,并定义了结构体方法 Lock() 和 Unlock(),源码如下所示:
type Mutex struct{
    state int32
    sema uint32
}

type Locker interface{
    Lock()
    Unlock()
}
实际应用中只需定义结构体 Mutex,分别调用结构体方法 Lock() 和 Unlock() 即可实现加锁处理,应用示例如下:
package main

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

// 定义互斥锁Mutex的全局变量
var (
   myMutex sync.Mutex
)

func get_data(name string) {
   // 加锁处理
   myMutex.Lock()
   // 程序执行
   fmt.Printf("这是:%v\n", name)
   // 解锁处理
   myMutex.Unlock()
}

func main() {
   // 执行并发
   go get_data("get_data")
   // 加锁处理
   myMutex.Lock()
   // 程序执行
   fmt.Printf("这是:%v\n", "Main")
   for i := 0; i < 3; i++ {
        // 每一秒输出一行数据
        time.Sleep(1 * time.Second)
        fmt.Printf("等待时间:%v秒\n", i+1)
   }
   // 解锁处理
   myMutex.Unlock()
   // 等待延时,为了等待并发程序执行完成
   // 可以改为WaitGroup等待
   time.Sleep(2 * time.Second)
}
运行上述代码,运行结果为:

这是:Main
等待时间:1秒
等待时间:2秒
等待时间:3秒
这是:get_data

根据运行结果分析上述代码:
1)定义全局变量 myMutex,变量类型为 sync.Mutex,它将在函数 get_data() 和主函数 main() 中使用。

2)定义函数 get_data(),由变量 myMutex 调用 Lock() 方法执行加锁处理,当函数执行完成后,再调用 Unlock() 方法执行解锁处理。

3)主函数 main() 首先执行并发处理,然后由变量 myMutex 调用 Lock() 方法执行加锁处理,由主函数 main() 占用资源执行遍历输出,最后调用 Unlock() 方法执行解锁处理,将资源释放,由并发程序的函数 get_data() 占用。

如果程序执行多个并发操作,由于每个并发的执行时间各不相同,sync.Mutex 只能保证当前只有一个并发占用资源,但不能改变并发的执行顺序,比如在上述代码的主函数 main() 中执行函数 get_data() 的多次并发。

主函数 main() 修改如下:
func main() {
   // 执行并发
   go get_data("AAA")
   go get_data("BBB")
   time.Sleep(2 * time.Second)
}
运行主函数 main(),字符串 AAA 和 BBB 的输出顺序各不相同,这也说明 sync.Mutex 无法保证并发程序的执行顺序。

我们再看读写互斥锁 sync.RWMutex,打开 sync.RWMutex 源码看到,如下所示:
type RWMutex struct{
     w Mutex
     writerSem uint32
     readerSem uint32
     readerCount int32
     readerWait int32
}
它的结构体成员 w 是结构体 Mutex,这说明 sync.RWMutex 是在 sync.Mutex 的基础上进行扩展的。

sync.RWMutex 提供了 4 个常用的结构体方法,分别为 RLock()、RUnlock()、Lock() 和 Unlock()。其中 RLock() 和 RUnlock() 支持数据多读模式,Lock() 和 Unlock() 支持数据单写模式,使用示例如下:
package main

import (
   "fmt"
   "math/rand"
   "sync"
   "time"
)

// 全局变量
var count int
// 定义读写锁
var rLock sync.RWMutex
// 定义同步等待组
var wg sync.WaitGroup
// 数据读取函数
func read(i int) {
   // 加锁
   rLock.RLock()
   // 设置延时
   t := time.Duration(i * 2) * time.Second
   time.Sleep(t)
   fmt.Printf("读操作,等待时间:%v 数据=%d\n", t.Seconds(), count)
   // 解锁
   rLock.RUnlock()
   wg.Done()
}

// 数据写入函数
func write(i int) {
   // 加锁
   rLock.Lock()
   // 写入数据
   count = rand.Intn(1000)
   // 设置延时
   t := time.Duration(i * 2) * time.Second
   time.Sleep(t)
   fmt.Printf("写操作,等待时间:%v 数据=%d\n", t.Seconds(), count)
   // 解锁
   rLock.Unlock()
   wg.Done()
}

func main() {
   // 设置同步等待组
   wg.Add(6)
   // 设置随机数种子,保证每次随机数不相同
   rand.Seed(time.Now().UnixNano())
   // 执行6次并发
   for i := 1; i < 4; i++ {
        go write(i)
   }
   for i := 1; i < 4; i++ {
        go read(i)
   }
   // 等待同步等待组执行并发
   wg.Wait()
}
运行上述代码,运行结果为:

写操作,等待时间:2 数据=719
读操作,等待时间:2 数据=719
读操作,等待时间:4 数据=719
读操作,等待时间:6 数据=719
写操作,等待时间:4 数据=395
写操作,等待时间:6 数据=417

从运行结果看到,sync.RWMutex 的读写操作不是同步执行的,并且每个操作的延时各不相同,说明如下:
sync.RWMutex 的读写操作不能只从字面上理解为数据的读取和写入,使用结构体方法 RLock() 和 RUnlock() 也能实现数据写入,但执行结果会出现误差,比如将上述代码的函数 write() 改用 RLock() 和 RUnlock(),程序运行结果为:

读操作,等待时间:2 数据=170
写操作,等待时间:2 数据=170
写操作,等待时间:4 数据=170
读操作,等待时间:4 数据=170
写操作,等待时间:6 数据=170
读操作,等待时间:6 数据=170

从运行结果看到,如果在数据写入的时候使用 RLock() 和 RUnlock(),每个并发的数据都会被最后一个并发的数据覆盖,因此我们会将 sync.RWMutex 的 RLock() 和 RUnlock() 作为数据读取,Lock() 和 Unlock() 作为数据写入。

在使用 RLock() 和 RUnlock()、Lock() 和 Unlock() 的时候,它们必须成对出现。如果使用 RLock() 加锁,Unlock() 解锁,程序会提示死锁异常(fatal error: all goroutines are asleep- deadlock!)。

相关文章