28. 学习 Go 协程:互斥锁和读写锁

Hi,你们好,我是明哥。git

在本身学习 Golang 的这段时间里,我写了详细的学习笔记放在个人我的微信公众号 《Go编程时光》,对于 Go 语言,我也算是个初学者,所以写的东西应该会比较适合刚接触的同窗,若是你也是刚学习 Go 语言,不防关注一下,一块儿学习,一块儿成长。github

个人在线博客:http://golang.iswbm.com
个人 Github:github.com/iswbm/GolangCodingTimegolang


在 「19. 学习 Go 协程:详解信道/通道」这一节里我详细地介绍信道的一些用法,要知道的是在 Go 语言中,信道的地位很是高,它是 first class 级别的,面对并发问题,咱们始终应该优先考虑使用信道,若是经过信道解决不了的,不得不使用共享内存来实现并发编程的,那 Golang 中的锁机制,就是你绕不过的知识点了。编程

今天就来说一讲 Golang 中的锁机制。数组

在 Golang 里有专门的方法来实现锁,仍是上一节里介绍的 sync 包。安全

这个包有两个很重要的锁类型微信

一个叫 Mutex, 利用它能够实现互斥锁。并发

一个叫 RWMutex,利用它能够实现读写锁。函数

1. 互斥锁 :Mutex

使用互斥锁(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 锁虽然很简单,但仍然有几点须要注意:

  • 同一协程里,不要在还没有解锁时再次使加锁
  • 同一协程里,不要对已解锁的锁再次解锁
  • 加了锁后,别忘了解锁,必要时使用 defer 语句

3. 读写锁:RWMutex

Mutex 是最简单的一种锁类型,他提供了一个傻瓜式的操做,加锁解锁加锁解锁,让你不须要再考虑其余的。

简单同时意味着在某些特殊状况下有可能会形成时间上的浪费,致使程序性能低下。

举个例子,咱们平时去图书馆,要嘛是去借书,要嘛去还书,借书的流程繁锁,没有办卡的还要让管理员给你办卡,所以借书一般都要排老长的队,假设图书馆里只有一个管理员,按照 Mutex(互斥锁)的思想, 这个管理员同一时刻只能服务一我的,这就意味着,还书的也要跟借书的一块儿排队。

可还书的步骤很是简单,可能就把书给管理员扫下码就能够走了。

若是让还书的人,跟借书的人一块儿排队,那估计有不少人都不乐意了。

所以,图书馆为了提升整个流程的效率,就容许还书的人,不须要排队,能够直接自助还书。

图书管将馆里的人分得更细了,对于读者的不一样需求提供了不一样的方案。提升了效率。

RWMutex,也是如此,它将程序对资源的访问分为读操做和写操做

  • 为了保证数据的安全,它规定了当有人还在读取数据(即读锁占用)时,不允计有人更新这个数据(即写锁会阻塞)
  • 为了保证程序的效率,多我的(线程)读取数据(拥有读锁)时,互不影响不会形成阻塞,它不会像 Mutex 那样只容许有一我的(线程)读取同一个数据。

理解了这个后,再来看看,如何使用 RWMutex?

定义一个 RWMuteux 锁,有两种方法

// 第一种
var lock *sync.RWMutex
lock = new(sync.RWMutex)

// 第二种
lock := &sync.RWMutex{}

RWMutex 里提供了两种锁,每种锁分别对应两个方法,为了不死锁,两个方法应成对出现,必要时请使用 defer。

  • 读锁:调用 RLock 方法开启锁,调用 RUnlock 释放锁
  • 写锁:调用 Lock 方法开启锁,调用 Unlock 释放锁(和 Mutex相似)

接下来,直接看一下例子吧

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 后,释放锁
程序退出...

系列导读

01. 开发环境的搭建(Goland & VS Code)

02. 学习五种变量建立的方法

03. 详解数据类型:****整形与浮点型

04. 详解数据类型:byte、rune与string

05. 详解数据类型:数组与切片

06. 详解数据类型:字典与布尔类型

07. 详解数据类型:指针

08. 面向对象编程:结构体与继承

09. 一篇文章理解 Go 里的函数

10. Go语言流程控制:if-else 条件语句

11. Go语言流程控制:switch-case 选择语句

12. Go语言流程控制:for 循环语句

13. Go语言流程控制:goto 无条件跳转

14. Go语言流程控制:defer 延迟调用

15. 面向对象编程:接口与多态

16. 关键字:make 和 new 的区别?

17. 一篇文章理解 Go 里的语句块与做用域

18. 学习 Go 协程:goroutine

19. 学习 Go 协程:详解信道/通道

20. 几个信道死锁经典错误案例详解

21. 学习 Go 协程:WaitGroup

22. 学习 Go 协程:互斥锁和读写锁

23. Go 里的异常处理:panic 和 recover

24. 超详细解读 Go Modules 前世此生及入门使用

25. Go 语言中关于包导入必学的 8 个知识点

26. 如何开源本身写的模块给别人用?

27. 说说 Go 语言中的类型断言?

28. 这五点带你理解Go语言的select用法


相关文章
相关标签/搜索