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日

相关文章

  • Huffman实现

    Huffman编码树 秒懂:【算法】Huffman编码_哔哩哔哩_bilibili 约定:字符x的编码长度 就是其对应叶节点的深度; 在一个字符集中,每个字符出现的次数有多有少,那么若都采用固定长度编码的话,那么编码长度会非常大,并且搜索时间复杂度都非常高;若采用非固定编码,出现次数多的字符编码长度小一些,并且放在树深度小的地方,提高搜索时间效率;这样带权平…

    算法与数据结构 2023年4月17日
    00
  • 深入解析MySQL索引数据结构

    深入解析MySQL索引数据结构 MySQL索引是优化查询效率的重要一环,本文将深入解析MySQL索引数据结构,帮助读者理解MySQL索引原理,并通过两个示例说明不同类型的索引在实际应用中的效果。 索引数据结构 MySQL支持两种类型的索引数据结构:B-Tree索引和Hash索引。 B-Tree索引 B-Tree索引是MySQL常用的索引类型,用于优化WHER…

    数据结构 2023年5月17日
    00
  • Java数据结构之实现跳表

    Java数据结构之实现跳表,是一篇对跳表数据结构的详细讲解。 背景 跳表是一种基于有序链表的高效查找算法,它的查找时间复杂度为O(logn),相比于普通链表的O(n),具有很大的优势。本文将介绍跳表的实现过程。 实现跳表 1. 跳表结构体 跳表的数据结构体实现包含以下四项: 头结点head:表示链表的起始位置。 节点Node:跳表中的节点,包含表层链表和下层…

    数据结构 2023年5月17日
    00
  • Oracle 11g Release (11.1) 索引底层的数据结构

    我来为您详细讲解“Oracle 11g Release (11.1) 索引底层的数据结构”的完整攻略。 索引底层数据结构简介 在Oracle数据库中,索引底层数据结构是B树(B-Tree)。B树是一种常用的多路平衡查找树,它的特点是每个节点都有多个子节点,能够自动调整高度,保持所有叶子节点到根节点的距离相等。在B树中,每个节点都有一个关键字列表和一个指向子节…

    数据结构 2023年5月17日
    00
  • 稀疏数组

    引入 当在网页上下棋类游戏时,玩到中途想要离开,但是我们需要保存进度,方便下次继续 我们应该怎么实现 ? 以围棋举例 使用二维数组将棋盘记下 ,如 0 为 没有棋子 ,1 为 黑子 , 2为白子 但是没有棋子的地方都为 0 ,整个二维数组充斥着大量的无效数据 0 我们需要想一个办法来 优化存储的方式 基本介绍 当一个数组中大部分元素是同一个值时,我们可以使用…

    算法与数据结构 2023年4月25日
    00
  • 浅析Java 数据结构常用接口与类

    浅析 Java 数据结构常用接口与类 本文主要介绍 Java 中常用的数据结构接口和类,可以帮助读者了解和掌握常见的数据结构以及它们的实现方式,从而在日后的开发中使用它们,提高代码的效率和质量。 List 接口 List 接口是 Java 中常用的数据结构接口之一,它代表了一个有序的集合,集合中的每一个元素都可以通过其索引进行访问。List 接口的一些常用方…

    数据结构 2023年5月17日
    00
  • Java数据结构之对象的比较

    Java数据结构之对象的比较 在Java中,对象的比较是非常重要的操作。我们常常需要对不同的对象进行比较,以便对它们进行排序、按照某个条件过滤等操作。本文将详细讲解Java中对象的比较,并给出一些示例来说明。 对象的比较方法 Java中有两种对象比较方法:值比较和引用比较。值比较就是比较两个对象的值是否相等,而引用比较是比较两个对象是否是同一个对象。 值比较…

    数据结构 2023年5月17日
    00
  • Java数据结构与算法之单链表深入理解

    Java数据结构与算法之单链表深入理解攻略 什么是单链表? 单链表(Singly Linked List)是指一个节点只指向下一个节点的链表。 单链表由多个节点组成,每个节点有两个属性:数据域和指针域。数据域保存节点的数据,指针域保存下一个节点的指针,因此每个节点包含两个域:data和next。 单链表的基本操作 单链表常用的基本操作包括: 在链表头部添加元…

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