ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

打开Go语言中的那把“锁” 打开Go语言中的那把“锁”

2021-10-24 23:59:41  阅读:239  来源: 互联网

标签:语言 Lock goroutine 互斥 Unlock Go 打开 等待



打开Go语言中的那把“锁”--互斥锁Mutex

操作系统中,关于进程间通信,是一个经常被谈起的问题。笔者也是在《现代操作系统》中第一次接触到这相关的内容。其中关于信号量、互斥锁等并发相关的内容,第一次接触也是从这里开始。

首先我们来看几个概念:
竞争条件:当两个或多个进程读写某些共享数据,而最后的结果取决于进程运行的精确时序,称为“竞争条件”。
注意理解,什么是精确时序?即进程执行的先后顺序会影响到最后的结果。
避免竞争条件,关键是阻止多个进程同时读取共享的数据。
这里我们把,对共享内存进行访问的程序片段称为“临界区”

如果同时访问临界区,就会造成访问或操作出错,为了避免这种情况发生,我们使用了“互斥锁”,限制临界区一次只能有一个进程/线程访问。
在《现代操作系统》一书中也有对互斥锁的描述,关于它的方法使用很简单,就像我们生活中的锁一样,只有开锁和解锁。本文主要从GO语言的角度来讨论互斥锁。
    
我们先来看一下Go语言中互斥锁Mutex的基本用法。

Mutex主要是实现了接口Locker

type Locker interface {
​    Lock()
​    Unlock()
}

这个接口很简单,简而言之就是进入临界区时加锁调用Lock(),离开临界区时解锁调用Unlock()。
关于为什么需要加锁,有并发编程经验的同学都有接触到,这里不做具体说明。
在Go中提供了一个检测并发资源是否有问题的工具race detector,在go run时添加-race就可以体验一下。

在实现一个线程安全的类时,可以采用嵌入字段的方式,将互斥锁sync.Mutex作为struct的一个对象。


我们再来看一下它的实现。总共经历过4个阶段的迭代优化。

1.第一版(使用一个flag字段标记是否持有锁)
2.给新goroutine机会(新的goroutine也能有机会竞争锁)
3.多给goroutine些机会(新来的和被唤醒的有机会获取更多锁)
4.解决饥饿(解决竞争问题,不会让goroutine长久等待)


1:第一版
mutex结构体包含2个字段:
key:用来标示这个排他锁是否被某个goroutine所持有。
sema:信号量变量,用来控制等待goroutine阻塞和休眠。(信号量用来管理等待的goroutine)

Lock()请求锁的时候,如果锁没有被goroutine持有,Lock()方法将key设置为1,这个goroutine就持有了该锁。
如果锁已经被其他goroutine所持有,当前goroutine会把key加1,而且调用semacquire()使用信号量将自己休眠,等锁释放的时候,信号量会将它唤醒。

Unlock()请求释放锁时,会将key减1,如果当前没有其他等待这个锁的goroutine这个方法就返回了。
但是如果还有其他goroutine在等待此锁,它会调用semrelease()利用信号量唤醒等待锁的其他goroutine中的一个。

使用时记住“谁申请谁释放”,Lock()和defer Unlock()搭配使用。
如果是临界区只是函数的一部分,则应该尽快释放。

2:给新goroutine机会
将初版的key字段换成了state字段。
state字段是一个复合型的字段,一个字段包含多个含义。
mutexWaiters(第三位):等待此锁的goroutine数
mutexWoken(第二位):唤醒标记(是否有唤醒的goroutine)
mutexLocked(最低位)这个锁是否被持有

加锁逻辑
1.如果没有goroutine持有锁,也没有等待持有锁的goroutine,那这个goroutine就很幸运,可以直接获得锁。
2.如果state不是零值,就通过一个循环进行检测,

请求锁的goroutine有两类,一类是新来请求锁的goroutine,另一类是被唤醒的等待请求锁的goroutine。

锁的状态也有两种,加锁和未加锁。
相对于初版,这次的改动主要就是:新来的goroutine也有机会先获得锁,打破了先来先得的逻辑。

3.多给goroutine些机会:
针对新来的goroutine,或者是被唤醒的goroutine,首次获取不到锁,会自旋获取,尝试一定自旋次数。

4.解决饥饿:
新的goroutine也会获得锁的机会,极端情况下,等待中的goroutine会一直获取不到锁,这就是饥饿问题。
让等待比较长的goroutine更有机会获得锁。

mutex的实现貌似非常复杂,其实主要还是针对饥饿模式和公平性问题,做了一些额外处理。


最后我们看一下比较常见的使用mutex出错的场景。
一.Lock/Unlock不成对
其中缺少Unlock的场景
1.代码中太多的if-else,可能在某个分支漏写了Unlock
2.重构时把Unlock给删除了
3.Unlock误写成了Lock
这种情况意味着其他goroutine永远获取不到锁

2.缺少Lock()的情况(触发panic)

二.复制已使用的mutex
package sync的同步原语,在使用后是不能被复制的。Go在运行时,有死锁的检查机制,使用go vet可以在编译的时候检查。

三.重入

java中的可重入锁,基本行为和互斥锁相同,加了一些扩展。
可重入锁:拥有该锁的线程,再次请求这把锁的话,不会阻塞,而是可以成功返回。只要拥有这把锁就可以一直用。
如何实现一个可重入锁?
1:记录获取锁的goroutine Id
2:提供一个token
可重入锁,解决了代码重入和递归调用带来的死锁问题。同时要求只有持有锁的goroutine可以解锁。
方式一:
0.获取goroutine Id(runtime.Stack方法获取)
1.先获取TLS对象
2.获取goroutine结构中的g指针
3.从g指针中取出goroutine id
方式二:通过用户传入的token,替换方式一的goroutine Id

三.死锁
两个或两个以上的进程(线程、goroutine)争夺共享资源而处于一种互相等待的状态,就被成为“死锁”。
死锁产生的4个条件:
1.互斥
2.持有和等待
3.不可剥夺(资源只能由持有它的goroutine来释放)
4.环路等待

本文针对go语言中的mutex只做了一个简单的介绍,主要参考了现代操作系统和Go并发编程实战课的内容,有需要的同学可以再依此来发散扩展。

标签:语言,Lock,goroutine,互斥,Unlock,Go,打开,等待
来源: https://blog.csdn.net/wen3qin/article/details/120944087

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有