Golang Mutex互斥锁源码分析

yizhihongxing

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
  • Go 数据结构之堆排序示例详解

    Go 数据结构之堆排序示例详解 什么是堆? 堆(Heap)是一种特殊的树形数据结构,它满足下列性质: 堆中每个节点的关键字都不大于(或不小于)其子节点的关键字。 堆中,根节点(顶端)是最小或最大元素。 堆实际上是一个完全二叉树,因此可以用数组实现。对于下标为i的节点,其左子节点为2i,右子节点为2i+1,父节点为i/2。 堆分为最大堆和最小堆。在最大堆中,父…

    数据结构 2023年5月17日
    00
  • java数据结构基础:稀疏数组

    Java数据结构基础:稀疏数组 在开发过程中,我们需要处理一些稀疏矩阵(大部分元素为0)的数据。这时候,使用稀疏数组是比较高效的方法。 什么是稀疏数组 稀疏数组是由很多元素值相同的元素组成,这些元素的值通常为0。而这些值不同时都存储在一个数组中会浪费很多内存空间。因此,我们使用稀疏数组来存储这些元素。 稀疏数组的定义: 稀疏数组的行数可以理解为矩阵的行数,而…

    数据结构 2023年5月17日
    00
  • MySQL索引详解及演进过程及面试题延伸

    MySQL索引详解及演进过程及面试题延伸 索引的作用 在 MySQL 中,索引是一种数据结构,可用于快速查找和访问表中的数据。使用索引可以大大提高查询效率,特别是在大型数据表中。 索引可以看作是一本书中的目录,目录中列出了每个章节的页码,通过查询目录,读者可以快速找到感兴趣的章节。 索引的种类 MySQL 中支持多种类型的索引,下面我们介绍一下常见的索引类型…

    数据结构 2023年5月17日
    00
  • C语言超详细讲解双向带头循环链表

    C语言双向带头循环链表 基本概念 带头双向循环链表是指在双向循环链表的基础上,在头节点前面添加一个头结点。这个头结点不存储任何数据,只是为了方便对链表进行操作。循环链表则是在单向或双向链表的基础上,使链表的头节点与尾节点相连,形成一个环。 综合这两种链表,就构成了“双向带头循环链表”这种数据结构。双向带头循环链表是一种灵活性较高的数据结构,支持前插、后插、前…

    数据结构 2023年5月17日
    00
  • Java实现链表数据结构的方法

    Java实现链表数据结构的方法可以分为以下步骤: 定义链表节点类Node 首先,在Java中实现链表数据结构,需要定义一个链表节点类,称为Node。Node类中包含两个重要属性: 数据域data,用于存储每个节点的数据信息。 指针域next,用于存储下一个节点的引用。 代码示例: public class Node { public int data; //…

    数据结构 2023年5月17日
    00
  • C语言数据结构之单链表的查找和建立

    C语言数据结构之单链表的查找和建立 什么是单链表? 单链表是一种常见的数据结构,是由若干个节点(Node)组成的链式结构,每个节点存储着链表中的元素和指向下一个节点的指针。 单链表的优点是插入、删除元素简单,但是查找元素比较困难。 在C语言中,我们可以使用结构体来定义一个节点: struct ListNode { int val; struct ListNo…

    数据结构 2023年5月17日
    00
  • C++数据结构之哈希表的实现

    以下是详细的讲解: C++数据结构之哈希表的实现 哈希表的概念 哈希表是一种能够实现快速查找的散列表,通过将关键字映射到哈希表中的一个位置来实现快速查找。哈希表的查询、删除时间复杂度为O(1),操作效率非常高,所以常常被用来对大量数据进行检索。 哈希表的实现 哈希函数 哈希函数的主要作用就是将任意长度的输入数据转化为固定长度的散列值,一般采用对关键字进行取模…

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