从零学习 Go 语言(28):学习 Go 协程中的互斥锁和读写锁

hello_wbm · · 1362 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

![](http://image.iswbm.com/20200607145423.png) 在线博客:http://golang.iswbm.com/ Github:https://github.com/iswbm/GolangCodingTime --- 在 「[**19. 学习 Go 协程:详解信道/通道**](http://mp.weixin.qq.com/s?__biz=MzU1NzU1MTM2NA==&mid=2247483741&idx=1&sn=4d4ccd8fdee404432f03447927ddb055&chksm=fc355b36cb42d2201e12b77085f7db5a5fed98674e369a5df7fd852abde09a1efad35ba28944&scene=21#wechat_redirect)」这一节里我详细地介绍信道的一些用法,要知道的是在 Go 语言中,信道的地位非常高,它是 first class 级别的,面对并发问题,我们始终应该优先考虑使用信道,如果通过信道解决不了的,不得不使用共享内存来实现并发编程的,那 Golang 中的锁机制,就是你绕不过的知识点了。 今天就来讲一讲 Golang 中的锁机制。 在 Golang 里有专门的方法来实现锁,还是上一节里介绍的 sync 包。 这个包有两个很重要的锁类型 一个叫 `Mutex`, 利用它可以实现互斥锁。 一个叫 `RWMutex`,利用它可以实现读写锁。 ## 1. 互斥锁 :Mutex 使用互斥锁(Mutex,全称 mutual exclusion)是为了来保护一个资源不会因为并发操作而引起冲突导致数据不准确。 举个例子,就像下面这段代码,我开启了三个协程,每个协程分别往 count 这个变量加1000次 1,理论上看,最终的 count 值应试为 3000 ```go package main import ( "fmt" "sync" ) func add(count *int, wg *sync.WaitGroup) { for i := 0; i < 1000; i++ { *count = *count + 1 } wg.Done() } func main() { var wg sync.WaitGroup count := 0 wg.Add(3) go add(&count, &wg) go add(&count, &wg) go add(&count, &wg) wg.Wait() fmt.Println("count 的值为:", count) } ``` 可运行多次的结果,都不相同 ```go // 第一次 count 的值为: 2854 // 第二次 count 的值为: 2673 // 第三次 count 的值为: 2840 ``` 原因就在于这三个协程在执行时,先读取 count 再更新 count 的值,而这个过程并不具备原子性,所以导致了数据的不准确。 解决这个问题的方法,就是给 add 这个函数加上 Mutex 互斥锁,要求同一时刻,仅能有一个协程能对 count 操作。 在写代码前,先了解一下 Mutex 锁的两种定义方法 ```go // 第一种 var lock *sync.Mutex lock = new(sync.Mutex) // 第二种 lock := &sync.Mutex{} ``` 然后就可以修改你上面的代码,如下所示 ```go import ( "fmt" "sync" ) func add(count *int, wg *sync.WaitGroup, lock *sync.Mutex) { for i := 0; i < 1000; i++ { lock.Lock() *count = *count + 1 lock.Unlock() } wg.Done() } func main() { var wg sync.WaitGroup lock := &sync.Mutex{} count := 0 wg.Add(3) go add(&count, &wg, lock) go add(&count, &wg, lock) go add(&count, &wg, lock) wg.Wait() fmt.Println("count 的值为:", count) } ``` 此时,不管你执行多少次,输出都只有一个结果 ```go count 的值为: 3000 ``` 使用 Mutext 锁虽然很简单,但仍然有几点需要注意: - 同一协程里,不要在尚未解锁时再次使加锁 - 同一协程里,不要对已解锁的锁再次解锁 - 加了锁后,别忘了解锁,必要时使用 defer 语句 ## 3. 读写锁:RWMutex Mutex 是最简单的一种锁类型,他提供了一个傻瓜式的操作,加锁解锁加锁解锁,让你不需要再考虑其他的。 **简单**同时意味着在某些特殊情况下有可能会造成时间上的浪费,导致程序性能低下。 举个例子,我们平时去图书馆,要嘛是去借书,要嘛去还书,借书的流程繁锁,没有办卡的还要让管理员给你办卡,因此借书通常都要排老长的队,假设图书馆里只有一个管理员,按照 Mutex(互斥锁)的思想, 这个管理员同一时刻只能服务一个人,这就意味着,还书的也要跟借书的一起排队。 可还书的步骤非常简单,可能就把书给管理员扫下码就可以走了。 如果让还书的人,跟借书的人一起排队,那估计有很多人都不乐意了。 因此,图书馆为了提高整个流程的效率,就允许还书的人,不需要排队,可以直接自助还书。 图书管将馆里的人分得更细了,对于读者的不同需求提供了不同的方案。提高了效率。 RWMutex,也是如此,它将程序对资源的访问分为读操作和写操作 - 为了保证数据的安全,它规定了当有人还在读取数据(即读锁占用)时,不允计有人更新这个数据(即写锁会阻塞) - 为了保证程序的效率,多个人(线程)读取数据(拥有读锁)时,互不影响不会造成阻塞,它不会像 Mutex 那样只允许有一个人(线程)读取同一个数据。 理解了这个后,再来看看,如何使用 RWMutex? 定义一个 RWMuteux 锁,有两种方法 ```go // 第一种 var lock *sync.RWMutex lock = new(sync.RWMutex) // 第二种 lock := &sync.RWMutex{} ``` RWMutex 里提供了两种锁,每种锁分别对应两个方法,为了避免死锁,两个方法应成对出现,必要时请使用 defer。 - 读锁:调用 RLock 方法开启锁,调用 RUnlock 释放锁 - 写锁:调用 Lock 方法开启锁,调用 Unlock 释放锁(和 Mutex类似) 接下来,直接看一下例子吧 ```go package main import ( "fmt" "sync" "time" ) func main() { lock := &sync.RWMutex{} lock.Lock() for i := 0; i < 4; i++ { go func(i int) { fmt.Printf("第 %d 个协程准备开始... \n", i) lock.RLock() fmt.Printf("第 %d 个协程获得读锁, sleep 1s 后,释放锁\n", i) time.Sleep(time.Second) lock.RUnlock() }(i) } time.Sleep(time.Second * 2) fmt.Println("准备释放写锁,读锁不再阻塞") // 写锁一释放,读锁就自由了 lock.Unlock() // 由于会等到读锁全部释放,才能获得写锁 // 因为这里一定会在上面 4 个协程全部完成才能往下走 lock.Lock() fmt.Println("程序退出...") lock.Unlock() } ``` 输出如下 ``` 第 1 个协程准备开始... 第 0 个协程准备开始... 第 3 个协程准备开始... 第 2 个协程准备开始... 准备释放写锁,读锁不再阻塞 第 2 个协程获得读锁, sleep 1s 后,释放锁 第 3 个协程获得读锁, sleep 1s 后,释放锁 第 1 个协程获得读锁, sleep 1s 后,释放锁 第 0 个协程获得读锁, sleep 1s 后,释放锁 程序退出... ``` ## 系列导读 --- [从零学习 Go 语言(01):一文搞定开发环境的搭建](https://studygolang.com/articles/27365) [从零学习 Go 语言(02):学习五种变量创建的方法](https://studygolang.com/articles/27432) [从零学习 Go 语言(03):数据类型之整型与浮点型](https://studygolang.com/articles/27440) [从零学习 Go 语言(04):byte、rune与字符串](https://studygolang.com/articles/27463) [从零学习 Go 语言(05):数据类型之数组与切片](https://studygolang.com/articles/27508) [从零学习 Go 语言(06):数据类型之字典与布尔类型](https://studygolang.com/articles/27563) [从零学习 Go 语言(07):数据类型之指针](https://studygolang.com/articles/27585) [从零学习 Go 语言(08):流程控制之if-else](https://studygolang.com/articles/27613) [从零学习 Go 语言(09):流程控制之switch-case](https://studygolang.com/articles/27660) [从零学习 Go 语言(10):流程控制之for 循环](https://studygolang.com/articles/28120) [从零学习 Go 语言(11):goto 无条件跳转](https://studygolang.com/articles/28472) [从零学习 Go 语言(12):流程控制之defer 延迟语句](https://studygolang.com/articles/28515) [从零学习 Go 语言(13):异常机制 panic 和 recover](https://studygolang.com/articles/28519) [从零学习 Go 语言(14):Go 语言中的类型断言是什么?](https://studygolang.com/articles/29305) [从零学习 Go 语言(15):学习 Go 语言的结构体与继承](https://studygolang.com/articles/29306) [从零学习 Go 语言(17):Go 语言中的 make 和 new 有什么区别?](https://studygolang.com/articles/29315) [从零学习 Go 语言(18):Go 语言中的 语句块与作用域](https://studygolang.com/articles/29365) [从零学习 Go 语言(19):Go Modules 前世今生及入门使用](https://studygolang.com/articles/29371) [从零学习 Go 语言(20):关于包导入必学的 8 个知识点](https://studygolang.com/articles/29404) [从零学习 Go 语言(21):一文了解 Go语言中编码规范](https://studygolang.com/articles/29477) [从零学习 Go 语言(22):Go 语言中如何开源自己写的包给别人用?](https://studygolang.com/articles/29609) [从零学习 Go 语言(23):一篇文章搞懂 Go 语言的函数](https://studygolang.com/articles/29628) [从零学习 Go 语言(24):理解 Go 语言中的 goroutine](https://studygolang.com/articles/29641) [从零学习 Go 语言(25):详解信道/通道](https://studygolang.com/articles/29704) [从零学习 Go 语言(26):通道死锁经典错误案例详解](https://studygolang.com/articles/29756) [从零学习 Go 语言(27):学习 Go 协程中的 WaitGroup](https://studygolang.com/articles/29783) --- ![](http://image.python-online.cn/20200321153457.png)

有疑问加站长微信联系(非本文作者))

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

1362 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传