导图社区 Mutex
Mutex主要整理了1、互斥锁的实现原理;2、正常模式和饥饿模式的区别;3、互斥锁允许自旋的条件;4、读写锁的实现原理;5、可重入锁如何实现;6、原子操作有哪些?7、原子操作和锁的区别 七个方面。
goroutine思维导图,本文整理了 1、底层实现原理? 2、goroutine和线程的区别? 3、goroutine泄漏的场景? 4、如何查看正在执行的goroutine数量? 5、如何控制并发的goroutine数量? 6、什么时候抢占P? 7、关于启动流程? 8、goroutine挂起? 相关知识可以导出使用。
MySQL面试题:聚簇索引:数据和索引存储到一起,找到索引就获取到了数据。聚簇索引是唯一的,InnoDB一定会有一个聚簇索引来保存数据。非聚簇索引一定存储有聚簇索引的列值。
社区模板帮助中心,点此进入>>
论语孔子简单思维导图
《傅雷家书》思维导图
《童年》读书笔记
《茶馆》思维导图
《朝花夕拾》篇目思维导图
《昆虫记》思维导图
《安徒生童话》思维导图
《鲁滨逊漂流记》读书笔记
《这样读书就够了》读书笔记
妈妈必读:一张0-1岁孩子认知发展的精确时间表
Mutex
1、互斥锁的实现原理
类型
都属于悲观锁
互斥锁
只能存在一个读或写,不能同时读和写
读写互斥锁
当一个 goroutine 获得了读锁后,其他 goroutine可以获取读锁,但不能获取写锁;当一个 goroutine 获得了写锁后,其他 goroutine既不能获取读锁也不能获取写锁(只能存在一个写者或多个读者,可以同时读)
底层实现结构
type Mutex struct { state int32 sema uint32 }
state表示锁的状态:有锁定、被唤醒、饥饿模式等,并且使用state的二进制位来标识的,不同模式有不同的处理方式

sema表示信号量,mutex阻塞队列的定位是通过这个变量来实现的, 从而实现goroutine的阻塞和唤醒。
操作
锁的实现一般会依赖于原子操作、信号量,通过atomic 包中的一些原子操作来实现锁的锁定,通过信号量来实现线程的阻塞与唤醒
加锁
通过原子操作cas加锁,如果加锁不成功,根据不同的场景选择自旋重试加锁或者阻塞等待被唤醒后加锁
解锁
通过原子操作add解锁,如果仍有goroutine在等待,唤醒等待的goroutine
注意点:
1、在 Lock() 之前使用 Unlock() 会导致 panic 异常
2、使用 Lock() 加锁后,再次 Lock() 会导致死锁(不支持重入),需Unlock()解锁后才能再加锁
3、锁定状态与 goroutine 没有关联,一个 goroutine 可以 Lock,另一个 goroutine 可以 Unlock
2、正常模式和饥饿模式的区别
两种抢锁的模式
正常模式
1、在刚开始的时候,是处于正常模式(Barging),也就是,当一个G1持有着一个锁的时候,G2会自旋的去尝试获取这个锁
2、当自旋超过4次 还没有能获取到锁的时候,这个G2就会被加入到获取锁的等待队列里面,并阻塞等待唤醒
饥饿模式
当一个 goroutine 等待锁时间超过 1 毫秒时,它可能会遇到饥饿问题。 在版本1.9中,这种场景下Go Mutex 切换到饥饿模式(handoff),解决饥饿问题。
回归正常模式的条件:
1、G的执行时间小于1ms
2、等待队列已经全部清空了
总结:
对于两种模式,正常模式下的性能是最好的,goroutine 可以连续多次获取锁,饥饿模式解决了取锁公平的问题,但是性能会下降,其实是性能和公平的 一个平衡模式。
3、互斥锁允许自旋的条件
线程没有获取到锁的2中处理方式
1、一种是没有获取到锁的线程就一直循环等待判断该资源是否已经释放锁,这种锁也叫做自旋锁 ,它不用将线程阻塞起来, 适用于并发低且程序执行时间短的场景,缺点是cpu占用较高
2、另外一种处理方式就是把自己阻塞起来,会释放CPU给其他线程 ,内核会将线程置为「睡眠」状态,等到锁被释放后,内核会在合适的时机唤醒该线程,适用于高并发场景,缺点是有线程上下文切换的开销
允许自旋的条件
1. 锁已被占用,并且锁不处于饥饿模式。
2. 积累的自旋次数小于最大自旋次数(active_spin=4)。
3. cpu 核数大于 1。
4. 有空闲的 P。
5. 当前 goroutine 所挂载的 P 下,本地待运行队列为空。
如果可以进入自旋状态之后就会调用 runtime_doSpin 方法进入自旋, doSpin 方法会调用 procyield(30) 执行30次 PAUSE 指令,什么都不做,但是会消耗CPU时间
4、读写锁的实现原理
使用场景
读多于写的情况(既保证线程安全,又保证性能不太差)
type RWMutex struct { w Mutex // 复用互斥锁 writerSem uint32 // 信号量,用于写等待读 readerSem uint32 // 信号量,用于读等待写 readerCount int32 // 当前执行读的 goroutine 数量 readerWait int32 // 被阻塞的准备读的 goroutine 的数量 }
通过记录 readerCount 读锁的数量来进行控制,当有一个写锁的时候,会将读 锁数量设置为负数 1<<30。目的是让新进入的读锁等待之前的写锁释放通知读 锁。同样的当有写锁进行抢占时,也会等待之前的读锁都释放完毕,才会开始 进行后续的操作。 而等写锁释放完之后,会将值重新加上 1<<30, 并通知刚才 新进入的读锁(rw.readerSem),两者互相限制。
5、可重入锁如何实现
概念
可重入锁又称为递归锁,是指在同一个线程在外层方法获取锁的时候,在进入该线程的内层方法时会自动获取锁,不会因为之前已经获取过还没释放再次加锁导致死锁
Go
Mutex 不是可重入的锁。Mutex 的实现中没有记录哪个 goroutine 拥有这把锁。理论上,任何 goroutine 都可以随意地 Unlock 这把锁,所以没办法计算重入条件,并且Mutex 重复Lock会导致死锁。
如何实现可重入锁
记住持有锁的线程
统计重入的次数
6、原子操作有哪些
Go atomic包是最轻量级的锁(也称无锁结构),可以在不形成临界区和创建互斥量的情况下完成并发安全的值替换操作,不过这个包只支持int32/int64/uint32/uint64/uintptr这几种数据类型的一些基础操作(增减、交换、载入、存储等)
原子操作仅会由一个独立的CPU指令代表和完成。原子操作是无锁的,常常直接通过CPU指令直接实现。 事实上,其它同步技术的实现常常依赖于原子操作。
常见操作
增减Add
载入Load
比较并交换CompareAndSwap
交换Swap
存储Store
7、原子操作和锁的区别
原子操作由底层硬件支持,而锁是基于原子操作+信号量完成的。若实现相同的功能,前者通常会更有效率
原子操作是单个指令的互斥操作;互斥锁/读写锁是一种数据结构,可以完成临界区(多个指令)的互斥操作,扩大原子操作的范围
原子操作是无锁操作,属于乐观锁;说起锁的时候,一般属于悲观锁
原子操作存在于各个指令/语言层级,比如“机器指令层级的原子操作”,“汇编指令层级的原子操作”,“Go语言层级的原子操作”等。
锁也存在于各个指令/语言层级中,比如“机器指令层级的锁”,“汇编指令层级的锁”,“Go语言层级的锁”等