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(),源码如下所示:
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() 修改如下:
我们再看读写互斥锁 sync.RWMutex,打开 sync.RWMutex 源码看到,如下所示:
sync.RWMutex 提供了 4 个常用的结构体方法,分别为 RLock()、RUnlock()、Lock() 和 Unlock()。其中 RLock() 和 RUnlock() 支持数据多读模式,Lock() 和 Unlock() 支持数据单写模式,使用示例如下:
sync.RWMutex 的读写操作不能只从字面上理解为数据的读取和写入,使用结构体方法 RLock() 和 RUnlock() 也能实现数据写入,但执行结果会出现误差,比如将上述代码的函数 write() 改用 RLock() 和 RUnlock(),程序运行结果为:
在使用 RLock() 和 RUnlock()、Lock() 和 Unlock() 的时候,它们必须成对出现。如果使用 RLock() 加锁,Unlock() 解锁,程序会提示死锁异常(fatal error: all goroutines are asleep- deadlock!)。
锁机制是由内置包 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 的读写操作不能只从字面上理解为数据的读取和写入,使用结构体方法 RLock() 和 RUnlock() 也能实现数据写入,但执行结果会出现误差,比如将上述代码的函数 write() 改用 RLock() 和 RUnlock(),程序运行结果为:
读操作,等待时间:2 数据=170
写操作,等待时间:2 数据=170
写操作,等待时间:4 数据=170
读操作,等待时间:4 数据=170
写操作,等待时间:6 数据=170
读操作,等待时间:6 数据=170
在使用 RLock() 和 RUnlock()、Lock() 和 Unlock() 的时候,它们必须成对出现。如果使用 RLock() 加锁,Unlock() 解锁,程序会提示死锁异常(fatal error: all goroutines are asleep- deadlock!)。