Hi,你们好,我是明哥。git
在本身学习 Golang 的这段时间里,我写了详细的学习笔记放在个人我的微信公众号 《Go编程时光》,对于 Go 语言,我也算是个初学者,所以写的东西应该会比较适合刚接触的同窗,若是你也是刚学习 Go 语言,不防关注一下,一块儿学习,一块儿成长。github
个人在线博客:golang.iswbm.com 个人 Github:github.com/iswbm/GolangCodingTimegolang
在 「19. 学习 Go 协程:详解信道/通道」这一节里我详细地介绍信道的一些用法,要知道的是在 Go 语言中,信道的地位很是高,它是 first class 级别的,面对并发问题,咱们始终应该优先考虑使用信道,若是经过信道解决不了的,不得不使用共享内存来实现并发编程的,那 Golang 中的锁机制,就是你绕不过的知识点了。编程
今天就来说一讲 Golang 中的锁机制。安全
在 Golang 里有专门的方法来实现锁,仍是上一节里介绍的 sync 包。bash
这个包有两个很重要的锁类型微信
一个叫 Mutex
, 利用它能够实现互斥锁。并发
一个叫 RWMutex
,利用它能够实现读写锁。函数
使用互斥锁(Mutex,全称 mutual exclusion)是为了来保护一个资源不会由于并发操做而引发冲突致使数据不许确。性能
举个例子,就像下面这段代码,我开启了三个协程,每一个协程分别往 count 这个变量加1000次 1,理论上看,最终的 count 值应试为 3000
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)
}
复制代码
可运行屡次的结果,都不相同
// 第一次
count 的值为: 2854
// 第二次
count 的值为: 2673
// 第三次
count 的值为: 2840
复制代码
缘由就在于这三个协程在执行时,先读取 count 再更新 count 的值,而这个过程并不具有原子性,因此致使了数据的不许确。
解决这个问题的方法,就是给 add 这个函数加上 Mutex 互斥锁,要求同一时刻,仅能有一个协程能对 count 操做。
在写代码前,先了解一下 Mutex 锁的两种定义方法
// 第一种
var lock *sync.Mutex
lock = new(sync.Mutex)
// 第二种
lock := &sync.Mutex{}
复制代码
而后就能够修改你上面的代码,以下所示
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)
}
复制代码
此时,无论你执行多少次,输出都只有一个结果
count 的值为: 3000
复制代码
使用 Mutext 锁虽然很简单,但仍然有几点须要注意:
Mutex 是最简单的一种锁类型,他提供了一个傻瓜式的操做,加锁解锁加锁解锁,让你不须要再考虑其余的。
简单同时意味着在某些特殊状况下有可能会形成时间上的浪费,致使程序性能低下。
举个例子,咱们平时去图书馆,要嘛是去借书,要嘛去还书,借书的流程繁锁,没有办卡的还要让管理员给你办卡,所以借书一般都要排老长的队,假设图书馆里只有一个管理员,按照 Mutex(互斥锁)的思想, 这个管理员同一时刻只能服务一我的,这就意味着,还书的也要跟借书的一块儿排队。
可还书的步骤很是简单,可能就把书给管理员扫下码就能够走了。
若是让还书的人,跟借书的人一块儿排队,那估计有不少人都不乐意了。
所以,图书馆为了提升整个流程的效率,就容许还书的人,不须要排队,能够直接自助还书。
图书管将馆里的人分得更细了,对于读者的不一样需求提供了不一样的方案。提升了效率。
RWMutex,也是如此,它将程序对资源的访问分为读操做和写操做
理解了这个后,再来看看,如何使用 RWMutex?
定义一个 RWMuteux 锁,有两种方法
// 第一种
var lock *sync.RWMutex
lock = new(sync.RWMutex)
// 第二种
lock := &sync.RWMutex{}
复制代码
RWMutex 里提供了两种锁,每种锁分别对应两个方法,为了不死锁,两个方法应成对出现,必要时请使用 defer。
接下来,直接看一下例子吧
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 后,释放锁
程序退出...
复制代码