Go语言并发编程之互斥锁Mutex和读写锁RWMutex
作者:Valine 发布时间:2024-04-25 15:00:32
在并发编程中,多个Goroutine
访问同一块内存资源时可能会出现竞态条件,我们需要在临界区中使用适当的同步操作来以避免竞态条件。Go 语言中提供了很多同步工具,本文将介绍互斥锁Mut
ex和读写锁RWMutex
的使用方法。
一、互斥锁Mutex
1、Mutex介绍
Go 语言的同步工具主要由 sync
包提供,互斥锁 (Mutex
) 与读写锁 (RWMutex
) 就是sync 包中的方法。
互斥锁可以用来保护一个临界区,保证同一时刻只有一个 goroutine
处于该临界区内。主要包括锁定(Lock方法)和解锁(Unlock方法)两个操作,首先对进入临界区的goroutine
进行锁定,离开时进行解锁。
使用互斥锁 (Mutex)时要注意以下几点:
不要重复锁定互斥锁,否则会阻塞,也可能会导致死锁(
deadlock
);要对互斥锁进行解锁,这也是为了避免重复锁定;
不要对未锁定或者已解锁的互斥锁解锁;
不要在多个函数之间直接传递互斥锁,
sync.Mutex
类型属于值类型,将它传给一个函数时,会产生一个副本,在函数中对锁的操作不会影响原锁
总之,一个互斥锁只用来保护一个临界区,加锁后记得解锁,对于每一个锁定操作,都要有且只有一个对应的解锁操作,也就是加锁和解锁要成对出现,最保险的做法时使用 defer
语句 解锁。
2、Mutex使用实例
下面的代码模拟取钱和存钱操作:
package main
import (
"flag"
"fmt"
"sync"
)
var (
mutex sync.Mutex
balance int
protecting uint // 是否加锁
sign = make(chan struct{}, 10) //通道,用于等待所有goroutine
)
// 存钱
func deposit(value int) {
defer func() {
sign <- struct{}{}
}()
if protecting == 1 {
mutex.Lock()
defer mutex.Unlock()
}
fmt.Printf("余额: %d\n", balance)
balance += value
fmt.Printf("存 %d 后的余额: %d\n", value, balance)
fmt.Println()
}
// 取钱
func withdraw(value int) {
defer func() {
sign <- struct{}{}
}()
if protecting == 1 {
mutex.Lock()
defer mutex.Unlock()
}
fmt.Printf("余额: %d\n", balance)
balance -= value
fmt.Printf("取 %d 后的余额: %d\n", value, balance)
fmt.Println()
}
func main() {
for i:=0; i < 5; i++ {
go withdraw(500) // 取500
go deposit(500) // 存500
}
for i := 0; i < 10; i++ {
<-sign
}
fmt.Printf("当前余额: %d\n", balance)
}
func init() {
balance = 1000 // 初始账户余额为1000
flag.UintVar(&protecting, "protecting", 0, "是否加锁,0表示不加锁,1表示加锁")
}
上面的代码中,使用了通道来让主 goroutine
等待其他 goroutine
运行结束,每个子goroutine
在运行结束之前向通道发送一个元素,主 goroutine
在最后从这个通道接收元素,接收次数与子goroutine
个数相同。接收完后就会退出主goroutine
。
代码使用协程实现多次(5次)对一个账户进行存钱和取钱的操作,先来看不加锁的情况:
余额: 1000
存 500 后的余额: 1500
余额: 1000
取 500 后的余额: 1000
余额: 1000
存 500 后的余额: 1500
余额: 1000
取 500 后的余额: 1000
余额: 1000
存 500 后的余额: 1500
余额: 1000
取 500 后的余额: 1000
余额: 1000
取 500 后的余额: 500
余额: 1000
存 500 后的余额: 1000
余额: 1000
取 500 后的余额: 500
余额: 1000
存 500 后的余额: 1000
当前余额: 1000
可以看到出现了混乱,比如第二次1000的余额取500后还是1000,这种对同一资源的竞争出现了竞态条件(Race Condition
)。
下面来看加锁的执行结果:
余额: 1000
取 500 后的余额: 500
余额: 500
存 500 后的余额: 1000
余额: 1000
取 500 后的余额: 500
余额: 500
存 500 后的余额: 1000
余额: 1000
取 500 后的余额: 500
余额: 500
存 500 后的余额: 1000
余额: 1000
存 500 后的余额: 1500
余额: 1500
取 500 后的余额: 1000
余额: 1000
取 500 后的余额: 500
余额: 500
存 500 后的余额: 1000
当前余额: 1000
加锁后就正常了。
下面介绍更细化的互斥锁:读/写互斥锁RWMutex。
二、读写锁RWMutex
1、RWMutex介绍
读/写互斥锁RWMutex
包含了读锁和写锁,分别对共享资源的“读操作”和“写操作”进行保护。sync.RWMutex
类型中的Lock方法和Unlock
方法分别用于对写锁进行锁定和解锁,而它的RLock
方法和RUnlock
方法则分别用于对读锁进行锁定和解锁。
有了互斥锁Mutex,为什么还需要读写锁呢?因为在很多并发操作中,并发读取占比很大,写操作相对较少,读写锁可以并发读取,这样可以提供服务性能。读写锁具有以下特征:
读写锁 | 读锁 | 写锁 |
---|---|---|
读锁 | Yes | No |
写锁 | No | No |
也就是说,
如果某个共享资源受到读锁和写锁保护时,其它
goroutine
不能进行写操作。换句话说就是读写操作和写写操作不能并行执行,也就是读写互斥;受读锁保护时,可以同时进行多个读操作。
在使用读写锁时,还需要注意:
不要对未锁定的读写锁解锁;
对读锁不能使用写锁解锁
对写锁不能使用读锁解锁
2、RWMutex使用实例
改写前面的取钱和存钱操作,添加查询余额的方法:
package main
import (
"fmt"
"sync"
)
// account 代表计数器。
type account struct {
num uint // 操作次数
balance int // 余额
rwMu *sync.RWMutex // 读写锁
}
var sign = make(chan struct{}, 15) //通道,用于等待所有goroutine
// 查看余额:使用读锁
func (c *account) check() {
defer func() {
sign <- struct{}{}
}()
c.rwMu.RLock()
defer c.rwMu.RUnlock()
fmt.Printf("%d 次操作后的余额: %d\n", c.num, c.balance)
}
// 存钱:写锁
func (c *account) deposit(value int) {
defer func() {
sign <- struct{}{}
}()
c.rwMu.Lock()
defer c.rwMu.Unlock()
fmt.Printf("余额: %d\n", c.balance)
c.num += 1
c.balance += value
fmt.Printf("存 %d 后的余额: %d\n", value, c.balance)
fmt.Println()
}
// 取钱:写锁
func (c *account) withdraw(value int) {
defer func() {
sign <- struct{}{}
}()
c.rwMu.Lock()
defer c.rwMu.Unlock()
fmt.Printf("余额: %d\n", c.balance)
c.num += 1
c.balance -= value
fmt.Printf("取 %d 后的余额: %d\n", value, c.balance)
fmt.Println()
}
func main() {
c := account{0, 1000, new(sync.RWMutex)}
for i:=0; i < 5; i++ {
go c.withdraw(500) // 取500
go c.deposit(500) // 存500
go c.check()
}
for i := 0; i < 15; i++ {
<-sign
}
fmt.Printf("%d 次操作后的余额: %d\n", c.num, c.balance)
}
执行结果:
余额: 1000
取 500 后的余额: 500
1 次操作后的余额: 500
1 次操作后的余额: 500
1 次操作后的余额: 500
1 次操作后的余额: 500
1 次操作后的余额: 500
余额: 500
存 500 后的余额: 1000
余额: 1000
取 500 后的余额: 500
余额: 500
存 500 后的余额: 1000
余额: 1000
存 500 后的余额: 1500
余额: 1500
取 500 后的余额: 1000
余额: 1000
取 500 后的余额: 500
余额: 500
存 500 后的余额: 1000
余额: 1000
取 500 后的余额: 500
余额: 500
存 500 后的余额: 1000
10 次操作后的余额: 1000
读写锁和互斥锁的不同之处在于读写锁把对共享资源的读操作和写操作分开了,可以实现更复杂的访问控制。
总结:
读写锁也是一种互斥锁,它是互斥锁的扩展。在使用时需要注意:
加锁后一定要解锁
不要重复加锁或者解锁
不解锁未锁定的锁
不要传递互斥锁
来源:https://www.tuicool.com/articles/YFnIJz7


猜你喜欢
- 这篇文章主要介绍了Python如何把多个PDF文件合并,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋
- 前言在python中, 切片是一个经常会使用到的语法, 不管是元组, 列表还是字符串, 一般语法就是:sequence[ilow:ihigh
- 背景index页面:首页品牌入口list页面:商品列表页面product页面:商品详情页面从index页面进入list的时候要刷新页面,从p
- 本文实例讲述了python使用pil生成缩略图的方法。分享给大家供大家参考。具体分析如下:这段代码实现python通过pil生成缩略图的功能
- Python 编程中使用 time 模块可以让程序休眠,具体方法是time.sleep(秒数),其中“秒数”以秒为单位,可以是小数,0.1秒
- 小主我总结了一下,看官仅供参考。具体运行时间,要看电脑,程序复杂程度,截图大小,原本为四个方法,后面又发现了一种。补上运行熟练度等因素。方法
- python的列表很重要,学习到后面你会发现使用的地方真的太多了。最近在写一些小项目时经常用到列表,有时其中的方法还会忘哎!所以为了复习写下
- 触发器是一种特殊类型的存储过程,它不同于之前的我们介绍的存储过程。触发器主要是通过事件进行触发被自动调用执行的。而存储过程可以通过存储过程的
- 题目描述682. 棒球比赛你现在是一场采用特殊赛制棒球比赛的记录员。这场比赛由若干回合组成,过去几回合的得分可能会影响以后几回合的得分。比赛
- pyecharts 是一个用于生成 Echarts 图表的类库。Echarts 是百度开源的一个数据可视化 JS 库。这篇文章重点给大家介绍
- 一、获取DataFrame列标签import pandas as pd file_path = '/Users/Arithmetic
- 问题:cudatoolkit cudnn 通过conda 虚拟环境安装,先前已经使用virtualenv安装tf,需要在conda虚拟环境中
- 一、本节说明我们在开发过程中经常需要监听用户的输入,比如:用户的点击事件、拖拽事件、键盘事件等等。这就需要用到我们下面要学习的内容v-on指
- 0.前置知识Go中的struct。mysql、Gin框架。Web基础。1.架构使用mysql作为数据库,Gin作为Web框架。2.功能模块1
- 解决tensoflow如何在已训练模型上继续训练fineturn的问题。训练代码任务描述: x = 3.0, y = 100.0, 运算公式
- 运行环境:eclipse+MySQL以前我们Java连接MySQL数据库都是一个数据库写一个类,类下面写好多方法,要是多个数据库,就要写多个
- 本文实例分析了python字典排序的方法。分享给大家供大家参考。具体如下:1、 准备知识:在python里,字典dictionary是内置的
- 一、MySQL中的日期时间类型MySQL中常用的几种时间类型有:date、datetime、time、year、timestampdatet
- 类似如下: select A.key,B.key,C.key from A,B,C where trim(A.key)=trim(B.fk)
- 今天使用import pandasusers = pandas.read_csv("H:\python\data analysis