Golang Mutex互斥锁源码分析

Golang Mutex互斥锁源码分析

介绍

Golang的Mutex互斥锁机制是一种非常重要的并发控制方式,它可以保证在同一时刻,同一共享资源只能被一个goroutine访问,其他的goroutine必须等待当前访问者释放锁之后才能访问该共享资源。

在使用Mutex机制时,需要进行锁定、解锁等操作,而这一过程是由Mutex的底层实现——sync包来完成的。

源码分析

Mutex实现了Locker接口,其底层包括一个int32类型的state字段(代表锁的状态)、一个chan类型的sema字段(用于goroutine的同步)及相关的操作方法Lock、Unlock等。下面我们就来详细分析Mutex的源码。

1. 变量定义

首先看一下Mutex的定义:

type Mutex struct {
    state int32
    sema  uint32
}
  • state是int32类型的字段,表示锁的状态,0表示未加锁状态,1表示已加锁状态。
  • sema是uint32类型的字段,用于goroutine的同步操作。当一个goroutine对该锁加锁时,sema会被减1,当goroutine解锁时,sema会被加1;当sema的值为0时,新的goroutine就必须等待已持有锁的goroutine释放锁后才能再次尝试加锁。

2. 方法实现

Lock

在加锁时,Mutex会首先检测是否已经被锁定,如果未被锁定,则获取锁,返回;如果已被锁定,则goroutine会被阻塞并放入wait list中,等待锁的释放。Mutex的Lock方法实现如下:

func (m *Mutex) Lock() {
    if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) { // 如果当前为未锁定状态,则设置为已锁定状态
        return
    }
    m.lockSlow() // 如果已被锁定,放入wait list中等待锁的释放
}

其中,mutexLocked是一个常量,表示Mutex已被锁定,其值为1(未锁定状态的值为0)。CompareAndSwapInt32是一个原子操作,用于比较并交换m.state的值,如果当前值为0,则将其设置为mutexLocked(1),此操作是原子的。原子操作可以保证同时只有一个goroutine可以执行。

如果当前状态已被设置为1,则执行lockSlow方法。

锁竞争

lockSlow是一个私有方法,它实现了Mutex的锁竞争控制,即竞争锁的goroutine会被阻塞并放入wait list中,等待锁的释放。其中,等待的goroutine状态为G_WAITING,表示它处于等待锁的状态。

func (m *Mutex) lockSlow() {
    awoke := false
    itr := runtime(0) // 等待的自旋次数
    for {
        if m.state < 0 || !atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) { // 锁已经被持有,将当前goroutine的状态设置为G_WAITING
            runtime.Gosched() // 让出CPU时间片,让其他goroutine有机会执行
            if !awoke { // 设置为可运行状态
                runtime_SemacquireMutex(&m.sema, false, itr+1)
                awoke = true
            } else { // 等待其他goroutine释放锁
                runtime_SemacquireMutex(&m.sema, true, 1)
            }
            itr = 0 // 重置自旋次数
            continue
        }
        if awoke { // 解锁
            runtime_Semrelease(&m.sema, false, 1)
        }
        break
    }
}

在等待锁的过程中,lockSlow在当前goroutine被阻塞之后,会进行一定的自旋等待,以便更快地响应锁的释放;同时,也会让出CPU时间片,给其他的goroutine执行机会。如果锁的状态变更了(可能是其他goroutine解锁了),那么锁等待的goroutine会被唤醒。

然后,程序将等待的goroutine状态设置为G_WAITING,代表该goroutine需要等待锁的释放,并调用runtime_SemacquireMutex方法进入等待状态。

Unlock

在解锁时,Mutex会首先使用CompareAndSwapInt32原子操作将持锁的状态设置为未加锁状态0。如果设置成功,则表示释放锁成功,将locked变量设置为unlock,则之前在wait list中等待的goroutine可以继续执行了。如果失败,说明锁已被其他goroutine持有,将lock状态设置为0,表示该goroutine不再持有锁了。Mutex的Unlock方法实现如下:

func (m *Mutex) Unlock() {
    if atomic.CompareAndSwapInt32(&m.state, mutexLocked, 0) {
        return
    }
    m.unlockSlow()
}

其中,如果解锁成功(即当前状态为mutexLocked),则直接返回;否则,调用unlockSlow方法。

解锁

unlockSlow是一个私有方法,实现了Mutex的解锁机制,即执行解锁操作的goroutine需要将锁的状态设置为0。对于处于wait list中等待锁的goroutine,unlockSlow方法会释放它们的等待状态,使它们能够继续执行。代码如下:

func (m *Mutex) unlockSlow() {
    for i := 0; ; i++ { // 从wait list中取出等待的goroutine解锁
        old := m.state
        new := int32(0)
        if old == 0 {
            panic("sync: unlock of unlocked mutex")
        }
        if old < 0 {
            new = old + mutexLocked // 等待goroutine的状态改为G_RUNNABLE,准备继续执行
        }
        if atomic.CompareAndSwapInt32(&m.state, old, new) {
            if old == mutexLocked {
                // unlock a locked mutex, no contention
                return
            }
            if new == 0 {
                // we've gone from a wait state to no wait state.
                // 从等待状态到非等待状态
                runtime_Semrelease(&m.sema, true, 1)
            }
            return
        }
        if i < 4 || runtime_canSpin(i) {
            runtime.Gosched() // 让出CPU时间片
        } else {
            time.Sleep(time.Duration(rand.Int63n(1)+1) * time.Millisecond) // 防止饥饿
        }
    }
}

如果解锁的当前状态已经被其他的goroutine修改,则会重试,直到成功为止。

示例分析

下面两个示例,分别展示Mutex的使用及其在goroutine间的同步机制。

示例一

package main

import (
    "fmt"
    "sync"
    "time"
)

var (
    x     int
    mutex sync.Mutex
)

func main() {
    fmt.Println("main start")

    go func() {
        mutex.Lock()
        fmt.Println("goroutine start")
        x += 1
        time.Sleep(time.Duration(2) * time.Second)
        fmt.Println("goroutine end")
        mutex.Unlock()
    }()

    time.Sleep(time.Duration(1) * time.Second)

    mutex.Lock()
    fmt.Println("main lock")
    x += 1
    fmt.Println("x:", x)
    mutex.Unlock()
    fmt.Println("main unlock")

    time.Sleep(time.Duration(3) * time.Second)
}

  • 该示例中包含一个全局int类型变量x和一个Mutex类型的变量mutex。
  • main goroutine先通过mutex锁住共享资源x,并对x进行自增操作,输出x的值。
  • 然后,该goroutine在mutex锁住的状态下,调用了一个子goroutine,在其中对x进行自增操作,并输出一些信息。
  • 子goroutine在两秒钟后自动释放了mutex的锁,main goroutine在三秒钟后也释放了mutex的锁。

运行结果如下:

main start
main lock
x: 1
main unlock
goroutine start
goroutine end

由于mutex被main goroutine锁定,子goroutine必须等待其释放锁之后才能访问共享资源x。在此示例中,我们可以清晰地看到Mutex的同步机制,确保了同一时刻只有一个goroutine正在使用共享资源x。

示例二

package main

import (
    "fmt"
    "sync"
    "time"
)

var wg sync.WaitGroup
var mutex sync.Mutex

func main() {
    wg.Add(2) // 添加两个goroutine

    go func() {
        mutex.Lock()
        defer mutex.Unlock()
        fmt.Println("goroutine 1 acquired lock")
        time.Sleep(time.Second)
        fmt.Println("goroutine 1 released lock")
        wg.Done()
    }()

    go func() {
        mutex.Lock()
        defer mutex.Unlock()
        fmt.Println("goroutine 2 acquired lock")
        time.Sleep(time.Second)
        fmt.Println("goroutine 2 released lock")
        wg.Done()
    }()

    wg.Wait()
    fmt.Println("main goroutine exit")
}
  • 该示例中包含两个goroutine,均对mutex进行锁定和解锁操作,共享mutex保护的变量。
  • 每个goroutine会先调用mutex的Lock方法获取互斥锁,并在调用启发式方法后,最终释放锁。

运行结果如下:

goroutine 1 acquired lock
goroutine 2 acquired lock
goroutine 1 released lock
goroutine 2 released lock
main goroutine exit

在该示例中,两个goroutine在几乎同时获取了mutex的锁,在mutex被其中一个goroutine释放之前,它们均处于等待状态。当一个goroutine释放了mutex的锁之后,另一个goroutine也才能获取到该锁,继而执行自己的逻辑。

总结

Mutex互斥锁是Golang中非常重要的并发控制方式,其底层实现是sync包。Mutex通过调用Lock、Unlock方法,实现了锁定和解锁等操作,使得同一时刻只有一个goroutine可以访问共享资源。Mutex的机制通过唤醒wait list中的等待goroutine,保证了goroutine之间的同步问题。通过本篇攻略的介绍,可以更好地理解Mutex的实现机制和运行原理。

本站文章如无特殊说明,均为本站原创,如若转载,请注明出处:Golang Mutex互斥锁源码分析 - Python技术站

(0)
上一篇 2023年5月17日
下一篇 2023年5月17日

相关文章

  • C语言实题讲解快速掌握单链表下

    C语言实题讲解快速掌握单链表下 简介 单链表是常见的一种数据结构,可以存储任意数量的数据,并且可以高效的进行插入、删除和查找操作。本篇文章将介绍如何使用C语言实现单链表,以及如何应对在实现单链表时所遇到的常见问题。 实现过程 数据结构设计 为了实现单链表,我们需要设计一个数据结构来存储节点信息,一般包含两个成员,一个是数据域,用来存储实际的数据,另一个是指针…

    数据结构 2023年5月17日
    00
  • Java链表数据结构及其简单使用方法解析

    Java链表数据结构及其简单使用方法解析 概述 链表是一种非线性结构,由一系列节点按照顺序连接而成。每个节点由数据域和指针域组成,数据域用于存储数据,指针域用于指向下一个节点或者上一个节点。在Java中,链表有多种实现方式,常见的有单向链表、双向链表等。 单向链表的实现 以下是一个单向链表的实现代码示例: public class Node { privat…

    数据结构 2023年5月17日
    00
  • C语言 超详细总结讲解二叉树的概念与使用

    C语言 超详细总结讲解二叉树的概念与使用 1. 什么是二叉树? 二叉树是一种树状数据结构,其中每个节点最多有两个子节点,被称为左子节点和右子节点。具有以下几个特点: 每个节点最多有两个子节点; 左子节点可以为空,右子节点也可以为空; 二叉树的每个节点最多有一个父节点; 二叉树通常定义为递归模式定义,即每个节点都可以看做一棵新的二叉树。 2. 二叉树的遍历方式…

    数据结构 2023年5月17日
    00
  • Lua教程(七):数据结构详解

    Lua教程(七):数据结构详解 Lua 中的数据结构广泛应用于各种计算机程序中。本文将详细介绍 Lua 中的数组、列表、栈、队列、集合和字典等数据结构的使用以及相关的函数。 数组 数组是存储在连续内存位置上的相同数据类型的元素集合。Lua 中的数组索引默认从 1 开始。下面是一些常用的 Lua 数组函数: table.concat(arr[, sep[, i…

    数据结构 2023年5月17日
    00
  • C语言数据结构之模式匹配字符串定位问题

    C语言数据结构之模式匹配字符串定位问题 什么是模式匹配字符串定位? 模式匹配字符串定位即在一个文本串中匹配一个模式串,并且返回模式串在文本串中第一次出现的位置。 例如,对于文本串“this is a test string”,我们想要匹配模式串“test”,我们期望得到的结果是第一次出现的位置为10。 KMP算法 算法思路 KMP算法是一种高效的字符串匹配算…

    数据结构 2023年5月16日
    00
  • 题目 3158: 蓝桥杯2023年第十四届省赛真题-三国游戏(贪心)

    题目描述 小蓝正在玩一款游戏。游戏中魏蜀吴三个国家各自拥有一定数量的士兵X, Y, Z (一开始可以认为都为 0 )。游戏有 n 个可能会发生的事件,每个事件之间相互独立且最多只会发生一次,当第 i 个事件发生时会分别让 X, Y, Z 增加Ai , Bi ,Ci 。当游戏结束时 (所有事件的发生与否已经确定),如果 X, Y, Z 的其中一个大于另外两个之…

    算法与数据结构 2023年4月30日
    00
  • C#常用数据结构和算法总结

    C#常用数据结构和算法总结 数据结构 数组(Array) 数组是一种线性数据结构,它可以在内存中连续地存储相同类型的数据。可以使用索引访问数组中的元素。数组的元素可以是任意类型。 在 C# 中,定义一个数组需要指定数组的类型和数组的大小。例如,定义一个包含 5 个整数的数组: int[] arr = new int[5]; 链表(LinkedList) 链表…

    数据结构 2023年5月17日
    00
  • Java数据结构之图的基础概念和数据模型详解

    Java数据结构之图的基础概念和数据模型详解 简介 图是一种非常重要的数据结构,在计算机科学和实际应用中广泛使用。比如搜索引擎中的网页之间的链接关系就可以用图来表示和处理。在本文中,我们将详细讲解图的基础概念和数据模型。同时,我们将通过两个实例来进一步说明图的应用。 图的基础概念 什么是图 图是由若干个节点(顶点)和连接节点的边组成的一种数据结构。一个图可以…

    数据结构 2023年5月17日
    00
合作推广
合作推广
分享本页
返回顶部