图解Go里面的互斥锁mutex了解编程语言核心实现源码
1. 锁的基础概念
1.1 CAS与轮询
1.1.1 cas实现锁
在锁的实现中现在越来越多的采用CAS来进行,通过利用处理器的CAS指令来实现对给定变量的值交换来进行锁的获取
1.1.2 轮询锁
在多线程并发的情况下很有可能会有线程CAS失败,通常就会配合for循环采用轮询的方式去尝试重新获取锁
1.2 锁的公平性
锁从公平性上通常会分为公平锁和非公平锁,主要取决于在锁获取的过程中,先进行锁获取的线程是否比后续的线程更先获得锁,如果是则就是公平锁:多个线程按照获取锁的顺序依次获得锁,否则就是非公平性
1.3 饥饿与排队
1.3.1 锁饥饿
锁饥饿是指因为大量线程都同时进行获取锁,某些线程可能在锁的CAS过程中一直失败,从而长时间获取不到锁
1.3.2 排队机制
上面提到了CAS和轮询锁进行锁获取的方式,可以发现如果已经有线程获取了锁,但是在当前线程在多次轮询获取锁失败的时候,就没有必要再继续进行反复尝试浪费系统资源,通常就会采用一种排队机制,来进行排队等待
1.4 位计数
在大多数编程语言中针对实现基于CAS的锁的时候,通常都会采用一个32位的整数来进行锁状态的存储
2. mutex实现
2.1 成员变量与模式
2.1.1 成员变量
在go的mutex中核心成员变量只有两个state和sema,其通过state来进行锁的计数,而通过sema来实现排队
type Mutex struct {
state int32
sema uint32
}
2.1.2 锁模式
锁模式主要分为两种
描述 | 公平性 | |
---|---|---|
正常模式 | 正常模式下所有的goroutine按照FIFO的顺序进行锁获取,被唤醒的goroutine和新请求锁的goroutine同时进行锁获取,通常新请求锁的goroutine更容易获取锁 | 否 |
饥饿模式 | 饥饿模式所有尝试获取锁的goroutine进行等待排队,新请求锁的goroutine不会进行锁获取,而是加入队列尾部等待获取锁 | 是 |
上面可以看到其实在正常模式下,其实锁的性能是最高的如果多个goroutine进行锁获取后立马进行释放则可以避免多个线程的排队消耗
同理在切换到饥饿模式后,在进行锁获取的时候,如果满足一定的条件也会切换回正常模式,从而保证锁的高性能
2.2 锁计数
2.2.1 锁状态
在mutex中锁有三个标志位,其中其二进制位分别位001(mutexLocked)、010(mutexWoken)、100(mutexStarving), 注意这三者并不是互斥的关系,比如一个锁的状态可能是锁定的饥饿模式并且已经被唤醒
mutexLocked = 1 << iota // mutex is locked
mutexWoken
mutexStarving
2.2.2 等待计数
mutex中通过低3位存储了当前mutex的三种状态,剩下的29位全部用来存储尝试正在等待获取锁的goroutine的数量
mutexWaiterShift = iota // 3
2.3唤醒机制
2.3.1 唤醒标志
唤醒标志其实就是上面说的第二位,唤醒标志主要用于标识当前尝试获取goroutine是否有正在处于唤醒状态的,记得上面公平模式下,当前正在cpu上运行的goroutine可能会先获取到锁
2.3.2 唤醒流程
当释放锁的时候,如果当前有goroutine正在唤醒状态,则只需要修改锁状态为释放锁,则处于woken状态的goroutine就可以直接获取锁,否则则需要唤醒一个goroutine, 并且等待这个goroutine修改state状态为mutexWoken,才退出
2.4 加锁流程
2.3.1 快速模式
如果当前没有goroutine加锁,则并且直接进行CAS成功,则直接获取锁成功
// Fast path: grab unlocked mutex.
if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) {
if race.Enabled {
race.Acquire(unsafe.Pointer(m))
}
return
}
2.3.2 自旋与唤醒
// 注意这里其实包含两个信息一个是如果当前已经是锁定状态,然后允许自旋iter主要是计数次数实际上只允许自旋4次
// 其实就是在自旋然后等待别人释放锁,如果有人释放锁,则会立刻进行下面的尝试获取锁的逻辑
if old&(mutexLocked|mutexStarving) == mutexLocked && runtime_canSpin(iter) {
// !awoke 如果当前线程不处于唤醒状态
// old&mutexWoken == 0如果当前没有其他正在唤醒的节点,就将当前节点处于唤醒的状态
// old>>mutexWaiterShift != 0 :右移3位,如果不位0,则表明当前有正在等待的goroutine
// atomic.CompareAndSwapInt32(&m.state, old, old|mutexWoken)设置当前状态为唤醒状态
if !awoke && old&mutexWoken == 0 && old>>mutexWaiterShift != 0 &&
atomic.CompareAndSwapInt32(&m.state, old, old|mutexWoken) {
awoke = true
}
// 尝试自旋,
runtime_doSpin()
// 自旋计数
iter++
// 从新获取状态
old = m.state
continue
}
2.3.3 更改锁状态
流程走到这里会有两种可能:
1.锁状态当前已经不是锁定状态
2.自旋超过指定的次数,不再允许自旋了
new := old
if old&mutexStarving == 0 {
// 如果当前不是饥饿模式,则这里其实就可以尝试进行锁的获取了|=其实就是将锁的那个bit位设为1表示锁定状态
new |= mutexLocked
}
if old&(mutexLocked|mutexStarving) != 0 {
// 如果当前被锁定或者处于饥饿模式,则增等待一个等待计数
new += 1 << mutexWaiterShift
}
if starving && old&mutexLocked != 0 {
// 如果当前已经处于饥饿状态,并且当前锁还是被占用,则尝试进行饥饿模式的切换
new |= mutexStarving
}
if awoke {
if new&mutexWoken == 0 {
throw("sync: inconsistent mutex state")
}
// awoke为true则表明当前线程在上面自旋的时候,修改mutexWoken状态成功
// 清除唤醒标志位
// 为什么要清除标志位呢?
// 实际上是因为后续流程很有可能当前线程会被挂起,就需要等待其他释放锁的goroutine来唤醒
// 但如果unlock的时候发现mutexWoken的位置不是0,则就不会去唤醒,则该线程就无法再醒来加锁
new &^= mutexWoken
}
2.3.3 加锁排队与状态转换
再加锁的时候实际上只会有一个goroutine加锁CAS成功,而其他线程则需要重新获取状态,进行上面的自旋与唤醒状态的重新计算,从而再次CAS
if atomic.CompareAndSwapInt32(&m.state, old, new) {
if old&(mutexLocked|mutexStarving) == 0 {
// 如果原来的状态等于0则表明当前已经释放了锁并且也不处于饥饿模式下
// 实际的二进制位可能是这样的 1111000, 后面三位全是0,只有记录等待goroutine的计数器可能会不为0
// 那就表明其实
break // locked the mutex with CAS
}
// 排队逻辑,如果发现waitStatrTime不为0,则表明当前线程之前已经再排队来,后面可能因为
// unlock被唤醒,但是本次依旧没获取到锁,所以就将它移动到等待队列的头部
queueLifo := waitStartTime != 0
if waitStartTime == 0 {
waitStartTime = runtime_nanotime()
}
// 这里就会进行排队等待其他节点进行唤醒
runtime_SemacquireMutex(&m.sema, queueLifo)
// 如果等待超过指定时间,则切换为饥饿模式 starving=true
// 如果一个线程之前不是饥饿状态,并且也没超过starvationThresholdNs,则starving为false
// 就会触发下面的状态切换
starving = starving || runtime_nanotime()-waitStartTime > starvationThresholdNs
// 重新获取状态
old = m.state
if old&mutexStarving != 0 {
// 如果发现当前已经是饥饿模式,注意饥饿模式唤醒的是第一个goroutine
// 当前所有的goroutine都在排队等待
// 一致性检查,
if old&(mutexLocked|mutexWoken) != 0 || old>>mutexWaiterShift == 0 {
throw("sync: inconsistent mutex state")
}
// 获取当前的模式
delta := int32(mutexLocked - 1<<mutexWaiterShift)
if !starving || old>>mutexWaiterShift == 1 {
// 如果当前goroutine不是饥饿状态,就从饥饿模式切换会正常模式
// 就从mutexStarving状态切换出去
delta -= mutexStarving
}
// 最后进行cas操作
atomic.AddInt32(&m.state, delta)
break
}
// 重置计数
awoke = true
iter = 0
} else {
old = m.state
}
2.5 释放锁逻辑
2.5.1 释放锁代码
func (m *Mutex) Unlock() {
if race.Enabled {
_ = m.state
race.Release(unsafe.Pointer(m))
}
// 直接进行cas操作
new := atomic.AddInt32(&m.state, -mutexLocked)
if (new+mutexLocked)&mutexLocked == 0 {
throw("sync: unlock of unlocked mutex")
}
if new&mutexStarving == 0 {
// 如果释放锁并且不是饥饿模式
old := new
for {
if old>>mutexWaiterShift == 0 || old&(mutexLocked|mutexWoken|mutexStarving) != 0 {
// 如果已经有等待者并且已经被唤醒,就直接返回
return
}
// 减去一个等待计数,然后将当前模式切换成mutexWoken
new = (old - 1<<mutexWaiterShift) | mutexWoken
if atomic.CompareAndSwapInt32(&m.state, old, new) {
// 唤醒一个goroutine
runtime_Semrelease(&m.sema, false)
return
}
old = m.state
}
} else {
// 唤醒等待的线程
runtime_Semrelease(&m.sema, true)
}
}
本文由博客一文多发平台 OpenWrite 发布!
图解Go里面的互斥锁mutex了解编程语言核心实现源码的更多相关文章
- 图解Go里面的sync.Map了解编程语言核心实现源码
基础筑基 在大多数语言中原始map都不是一个线程安全的数据结构,那如果要在多个线程或者goroutine中对线程进行更改就需要加锁,除了加1个大锁,不同的语言还有不同的优化方式, 像在java和go这 ...
- 图解Go语言的context了解编程语言核心实现源码
基础筑基 基于线程的编程语言中的一些设计 ThreadGroup ThreadGroup是基于线程并发的编程语言中常用的一个概念,当一个线程派生出一个子线程后通常会加入父线程的线程组(未指定线程组的情 ...
- 互斥锁Mutex与信号量Semaphore的区别
转自互斥锁Mutex与信号量Semaphore的区别 多线程编程中,常常会遇到这两个概念:Mutex和Semaphore,两者之间区别如下: 有人做过如下类比: Mutex是一把钥匙,一个人拿了就可进 ...
- 深入理解Solaris内核中互斥锁(mutex)与条件变量(condvar)之协同工作原理
在Solaris上写内核模块总是会用到互斥锁(mutex)与条件变量(condvar), 光阴荏苒日月如梭弹指一挥间,Solaris的大船说沉就要沉了,此刻心情不是太好(Orz).每次被年轻的有才华的 ...
- Linux内核互斥锁--mutex
一.定义: /linux/include/linux/mutex.h 二.作用及访问规则: 互斥锁主要用于实现内核中的互斥访问功能.内核互斥锁是在原子 API 之上实现的,但这对于内核用户是不可见 ...
- 线程锁(互斥锁Mutex)及递归锁
一.线程锁(互斥锁) 在一个程序内,主进程可以启动很多个线程,这些线程都可以访问主进程的内存空间,在Python中虽然有了GIL,同一时间只有一个线程在运行,可是这些线程的调度都归系统,操作系统有自身 ...
- 线程锁(互斥锁Mutex)
线程锁(互斥锁Mutex) 一个进程下可以启动多个线程,多个线程共享父进程的内存空间,也就意味着每个线程可以访问同一份数据,此时,如果2个线程同时要修改同一份数据,会出现什么状况? # -*- cod ...
- Golang 读写锁RWMutex 互斥锁Mutex 源码详解
前言 Golang中有两种类型的锁,Mutex (互斥锁)和RWMutex(读写锁)对于这两种锁的使用这里就不多说了,本文主要侧重于从源码的角度分析这两种锁的具体实现. 引子问题 我一般喜欢带着问题去 ...
- 一文带你剖析LiteOS互斥锁Mutex源代码
摘要:多任务环境下会存在多个任务访问同一公共资源的场景,而有些公共资源是非共享的临界资源,只能被独占使用.LiteOS使用互斥锁来避免这种冲突,互斥锁是一种特殊的二值性信号量,用于实现对临界资源的独占 ...
随机推荐
- Azkaban3.x
Azkaban3.x安装部署 官方文档地址 三种模式 solo-server模式:exec进程和web进程为同一个进程,存放元数据的数据库为H2 two-server模式:与之前的单机版本类似,exe ...
- css技巧——垂直居中
1.父元素确定的单行垂直居中 通过设置父元素的 height 和 line-height 高度一致来实现的. 2.父元素确定的多行垂直居中 父元素高度确定的多行文本.图片.块状元素的竖直居中的方法有两 ...
- 数组的查找,删除 Day07
package com.sxt.arraytest2; /* * 形参列表的作用:1.接受方法调用处传来的实参 * 2.规定了实参传入数据的类型 */ import java.util.Arrays; ...
- Mysql统计信息处理及binlog解释
TODO use db_name; -- 分析表 ANALYZE TABLE table_name; -- 查看表信息 ; -- 查看索引 SHOW INDEX FROM table_name; ht ...
- selenium webdriver学习(九)------------如何操作cookies(转)
selenium webdriver学习(九)------------如何操作cookies 博客分类: Selenium-webdriver Web 测试中我们经常会接触到Cookies,一个C ...
- 容器服务kubernetes federation v2实践五:多集群流量调度
概述 在federation v2多集群环境中,通过前面几篇文章的介绍,我们可以很容易的进行服务多集群部署,考虑到业务部署和容灾需要,我们通常需要调整服务在各个集群的流量分布.本文下面简单介绍如何在阿 ...
- sublime 插件安装packagecontrol
https://packagecontrol.io/installation 第一步: Installation Simple The simplest method of installation ...
- [kuangbin带你飞]专题九 连通图C - Critical Links UVA - 796
这道题就是要求桥的个数. 那么桥相应的也有判定的定理: 在和u相邻的节点中,存在一个节点是最小的时间戳都比 当前u的访问次序要大,也就是说这个点是只能通过果u到达,那么 他们之间相邻的边就是的桥 #i ...
- title与h1的区别、b与strong的区别、i与em的区别?
title与h1的区别 定义: title是网站标题, h1是文章主题 作用: title概括网站信息,可以直接告诉搜索引擎和用户这 个网站是关于什么主题和内容的,是显示在网页Tab栏里的: h1突出 ...
- java多异常处理
声明异常时尽可能声明具体异常类型,方便更好的处理; 方法声明几个异常就对应有几个catch块; 若多个catch块中的异常出现继承关系,父类异常catch块放在最后; 在catch语句块使用Excep ...