深入Golang之Mutex

这篇具有很好参考价值的文章主要介绍了深入Golang之Mutex。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

深入Golang之Mutex

深入Golang之Mutex,Golang,golang,开发语言,后端

基本使用方法

可以限制临界区只能同时由一个线程持有。

  • 直接在流程结构中使用 lockunlock
  • 嵌入到结构中,然后通过结构体的 mutex 属性 调用 lockunlock
  • 嵌入到结构体中,但是是直接在需要锁定的资源方法中使用,让外界无需关注资源锁定

在进行资源锁定的过程中,很容易出现 data race,这时候我们可以使用 race detector ,融入到 持续集成 中,以减少代码的 Bug

看实现

深入Golang之Mutex,Golang,golang,开发语言,后端

初版互斥锁

设立持有锁的标识 flagsema 信号量来控制互斥,实际上是利用 CAS 指令完成原子计算。

  • 字段 key:是一个 flag,用来标识这个排外锁是否被某个 goroutine 所持有,如果 key 大于等于 1,说明这个排外锁已经被持有; key 不仅仅标识了锁是否被 goroutine 所持有,还记录了当前持有和等待获取锁
    goroutine 的数量
  • 字段 sema:是个信号量变量,用来控制等待 goroutine 的阻塞休眠和唤醒。

Unlock 方法可以被任意的 goroutine 调用释放锁,即使是没持有这个互斥锁的 goroutine,也可以进行这个操作。这是因为,Mutex 本身并没有包含持有这把锁的 goroutine 的信息,所以,Unlock 也不会对此进行检查。Mutex 的这个设计一直保持至今。

由于上面这个原因,就有可能出现 if 判断中释放其他 goroutine,释放锁的 goroutine 不必是锁的持有者

func lockTest()
{
	lock()
	var count
	
	if count {
	    unlock()	
    }
	
	// 此处就可能出现 goroutine 释放其他的锁
	unlock()
}

四种常见使用错误

Lock/Unlock 不是成对出现的,漏写、意外删除

Copy已使用的 Mutex

type Counter struct { 
	sync.Mutex
	Count int
}
func main() { 
	var c Counter
	c.Lock()
	defer c.Unlock()
	c.Count++
	foo(c) // 复制锁
}
// 这里Counter的参数是通过复制的方式传入的
func foo(c Counter) { 
	c.Lock() 
	defer c.Unlock()
	fmt.Println("in foo")
}

为什么它不能被复制?

原因在于 Mutex 是一个有状态的对象,它的 state 字段记录这个锁的状态。如果你要复制一个已经加锁的 Mutex 给一个新的变量,那么新的刚初始化的变量居然被加锁了,这显然不符合预期

重入

  • 可重入锁概念解释

当一个线程获取锁时,如果没有其他线程拥有这个锁,那么这个线程就成功获取了这个锁,之后,如果其他线程再去请求这个锁,就会处于阻塞状态。如果拥有这把锁的线程再请求这把锁的话,不会阻塞,而是成功返回,所以叫可重入锁。

  • Mutex 不是可重入锁

想想也不奇怪,因为 Mutex 的实现中没有记录哪个 goroutine 拥有这把锁。理论上,任何 goroutine 都可以随意地 Unlock 这把锁,所以没办法计算重入条件

func foo(l sync.Locker) {
	fmt.Println("in foo")
	l.Lock()
	bar(l)
	l.Unlock()
}
// 这就是可重入锁
func bar(l sync.Locker) {
	l.Lock()
	fmt.Println("in bar")
	l.Unlock()
}
func main() {
	l := &sync.Mutex{}
	foo(l)
}
自己实现可重入锁
  • 通过 goroutine id

// RecursiveMutex 包装一个Mutex,实现可重入
type RecursiveMutex struct {
	sync.Mutex
	owner     int64 // 当前持有锁的goroutine id
	recursion int32 // 这个goroutine 重入的次数
}

func (m *RecursiveMutex) Lock() {
	gid := goid.Get() // 如果当前持有锁的goroutine就是这次调用的goroutine,说明是重入
	if atomic.LoadInt64(&m.owner) == gid {
		m.recursion++
		return
	}
	m.Mutex.Lock() // 获得锁的goroutine第一次调用,记录下它的goroutine id,调用次数加1
	atomic.StoreInt64(&m.owner, gid)
	m.recursion = 1
}

func (m *RecursiveMutex) Unlock() {
	gid := goid.Get() // 非持有锁的goroutine尝试释放锁,错误的使用
	if atomic.LoadInt64(&m.owner) != gid {
		panic(fmt.Sprintf("wrong the owner(%d): %d!", m.owner, gid))
	} // 调用次数减1
	m.recursion--
	if m.recursion != 0 { // 如果这个goroutine还没有完全释放,则直接返回
		return
	} // 此goroutine最后一次调用,需要释放锁
	atomic.StoreInt64(&m.owner, -1)
	m.Mutex.Unlock()
}

有一点,要注意,尽管拥有者可以多次调用 Lock,但是也必须调用相同次数的 Unlock,这样才能把锁释放掉。这是一个合理的设计,可以保证 LockUnlock 一一对应。

  • 方案二:token

这个与 goroutine id 差不多, goroutine id 既然没有暴露出来,说明设计方不希望使用这个,而这只是可重入锁的一个标识,我们可以自定义这个标识,由协程自己提供,在调用 lockunlock 中,自己传入一个生成的 token 即可,逻辑是一样的

死锁

  • 互斥: 排他性资源
  • 环路等待: 形成环路
  • 持有和等待: 持有还去和其他资源竞争
  • 不可剥夺: 资源只能由持有它的 goroutine 释放

打破以上条件其中一个或者几个即可解除死锁

扩展 Mutex

  • 实现 TryLock
  • 获取等待者的数量等指标
  • 使用 Mutex 实现一个线程安全的队列

读写锁的实现原理及避坑指南

标准库中的 RWMutex 是一个 reader/writer 互斥锁。RWMutex 在某一时刻只能由任意数量的 reader 持有,或者是只被单个的 writer 持有。
他是基于 Mutex 的。如果你遇到可以明确区分 readerwriter goroutine 的场景,且有大量的并发读、少量的并发写,并且有强烈的性能需求,你就可以考虑使用读写锁 RWMutex 替换 Mutex

读写锁的实现方式

  • Read-preferring:读优先的设计可以提供很高的并发性,但是,在竞争激烈的情况下可能会导致写饥饿。这是因为,如果有大量的读,这种设计会导致只有所有的读都释放了锁之后,写才可能获取到锁。
  • Write-preferring:写优先的设计意味着,如果已经有一个 writer 在等待请求锁的话,它会阻止新来的请求锁的 reader 获取到锁,所以优先保障 writer。当然,如果有一些 reader 已经请求了锁的话,新请求的 writer 也会等待已经存在的 reader 都释放锁之后才能获取。所以,写优先级设计中的优先权是针对新来的请求而言的。这种设计主要避免了 writer 的饥饿问题。
  • 不指定优先级:这种设计比较简单,不区分 reader 和 writer 优先级,某些场景下这种不指定优先级的设计反而更有效,因为第一类优先级会导致写饥饿,第二类优先级可能会导致读饥饿,这种不指定优先级的访问不再区分读写,大家都是同一个优先级,解决了饥饿的问题。

Go 标准库中的 RWMutex 设计是 Write-preferring 方案。一个正在阻塞的 Lock 调用会排除新的 reader 请求到锁。

RWMutex 的 3 个踩坑点

  • 不可复制
  • 重入导致死锁
  • 释放未加锁的 RWMutex

我们知道,有活跃 reader 的时候,writer 会等待,如果我们在 reader 的读操作时调用 writer 的写操作(它会调用 Lock 方法),那么,这个 readerwriter 就会形成互相依赖的死锁状态。Reader 想等待 writer 完成后再释放锁,而 writer 需要这个 reader 释放锁之后,才能不阻塞地继续执行。这是一个读写锁常见的死锁场景。

第三种死锁的场景更加隐蔽。
当一个 writer 请求锁的时候,如果已经有一些活跃的 reader,它会等待这些活跃的 reader 完成,才有可能获取到锁,但是,如果之后活跃的 reader 再依赖新的 reader 的话,这些新的 reader 就会等待 writer 释放锁之后才能继续执行,这就形成了一个环形依赖: writer 依赖活跃的 reader -> 活跃的 reader 依赖新来的 reader -> 新来的 reader 依赖 writer。文章来源地址https://www.toymoban.com/news/detail-672330.html

到了这里,关于深入Golang之Mutex的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 【Golang】VsCode下开发Go语言的环境配置(超详细图文详解)

    📓推荐网站(不断完善中):个人博客 📌个人主页:个人主页 👉相关专栏:CSDN专栏、个人专栏 🏝立志赚钱,干活想躺,瞎分享的摸鱼工程师一枚 ​ 话说在前,Go语言的编码方式是 UTF-8 ,理论上你直接使用文本进行编辑也是可以的,当然为了提升我们的开发效率我们还是需

    2024年02月07日
    浏览(83)
  • 100天精通Golang(基础入门篇)——第15天:深入解析Go语言中函数的应用:从基础到进阶,助您精通函数编程!(进阶)

    🌷 博主 libin9iOak带您 Go to Golang Language.✨ 🦄 个人主页——libin9iOak的博客🎐 🐳 《面试题大全》 文章图文并茂🦕生动形象🦖简单易学!欢迎大家来踩踩~🌺 🌊 《IDEA开发秘籍》学会IDEA常用操作,工作效率翻倍~💐 🪁 希望本文能够给您带来一定的帮助🌸文章粗浅,敬请批

    2024年02月12日
    浏览(67)
  • 【后端学习笔记·Golang】邮箱邮件验证

    流程: 接收用户请求后生成随机验证码,并将验证码存入Redis中,并设置TTL 通过gomail发送验证码给用户邮箱 接收用户输入的验证码,与Redis中存放的验证码进行比对 ​ 随机种子通过 time.Now().UnixNano() 进行设置,以确保对于同一个用户每次请求都使用不同的种子。然后,定义

    2024年04月26日
    浏览(50)
  • golang iris框架 + linux后端运行

    打包应用 开启服务 关闭后台 杀死进程 通过 ps -ef|grep main 找到应用出现 找到应用的( PID(一般是第一个数字) )

    2024年02月07日
    浏览(54)
  • Golang学习+深入(十一)-文件

    目录 一、概述 1、文件 1.1、打开文件和关闭文件 1.2、读文件 1.3、写文件 1.4、判断文件是否存在 1.5、拷贝文件 文件: 文件是数据源(保存数据的地方) 的一种,比如word文档,txt文件,excel文件...都是文件。文件最主要的作用就是保存数据,它既可以保存一张图片,也可以保存

    2023年04月14日
    浏览(53)
  • 深入理解 Golang: Goroutine 协程

    进程用来分配内存空间,是操作系统分配资源的最小单位;线程用来分配 CPU 时间,多个线程共享内存空间,是操作系统或 CPU 调度的最小单位;协程用来精细利用线程。协程就是将一段程序的运行状态打包,可以在线程之间调度。或者说将一段生产流程打包,使流程不固定在

    2024年02月11日
    浏览(81)
  • 深入理解 Golang: 网络编程

    关于计算机网络分层与 TCP 通信过程过程此处不再赘述。 考虑到 TCP 通信过程中各种复杂操作,包括三次握手,四次挥手等,多数操作系统都提供了 Socket 作为 TCP 网络连接的抽象。 Linux - Internet domain socket - SOCK_STREAM Linux 中 Socket 以 “文件描述符” FD 作为标识 在进行 Socket 通

    2024年02月11日
    浏览(40)
  • 深入了解Golang中的反射机制

    目录 反射 反射的分类 值反射 类型反射 运行时反射 编译时反射 接口反射 结构体反射 常用函数 值反射 类型反射 值反射和类型反射的区别 结构体反射 示例代码         反射是指在程序运行时动态地检查和修改对象的能力。在Go语言中,通过反射可以在运行时检查变量的

    2024年02月06日
    浏览(48)
  • 深入了解Golang atomic原子操作

       在编程中经常遇到并发而产生的问题,那么应该怎么解决并发呢?什么情况下会产生并发以及为什么会有并发问题的产生?下面我将从宏观的角度讲解并发安全性问题。    并发是指在同一时间间隔内,多个任务(线程、进程或活动)同时存在和执行的状态,在同一个

    2024年02月21日
    浏览(47)
  • 深入理解Golang中的接口与实例展示

    标题:深入理解Golang中的接口与实例展示 引言: Golang(Go)的接口是一项强大的特性,它为面向对象编程带来了灵活性和可维护性。本文将深入讲解Golang中的接口概念,从基础到实际应用,通过详细案例展示,帮助读者更好地掌握接口的使用和设计。 一、接口基础概念: 接

    2024年01月21日
    浏览(46)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包