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日

相关文章

  • Java数据结构的十大排序

    Java数据结构的十大排序攻略 简介 在计算机科学中,排序算法是一种将一串数据按照特定顺序进行排列的方法,其中常见的排序算法有很多种,不同的算法适用于不同的数据类型和数据规模。Java是一种常见的编程语言,也提供了很多实现排序算法的类和方法。 本文将介绍Java数据结构的十大排序算法,分别为:插入排序、希尔排序、选择排序、冒泡排序、快速排序、归并排序、堆排序…

    数据结构 2023年5月17日
    00
  • 2021最新Android笔试题总结美团Android岗职能要求

    2021最新Android笔试题总结和美团Android岗职能要求 简介 本文主要介绍了2021最新的Android笔试题总结和美团Android岗职能要求,旨在为正在面试美团Android岗位的面试者提供参考。 笔试题总结 下面是近期美团Android面试中出现的一些笔试题目: 1. 请描述Android中BroadcastReceiver的生命周期。 安…

    数据结构 2023年5月17日
    00
  • Java 中很好用的数据结构(你绝对没用过)

    Java 中很好用的数据结构(你绝对没用过) 介绍 Java 中的数据结构有很多,比如数组、链表、栈、队列、堆、树等等。在这些常见的数据结构中,我们或多或少都会使用到。但是本篇文章要讲述的是一些比较冷门,但是很好用的数据结构。 双向队列(Deque) 双向队列,顾名思义,是一种可以双向操作的队列。它可以从队列的两端插入和删除元素,因此常被用作实现栈和队列以及…

    数据结构 2023年5月17日
    00
  • Python 数据结构之旋转链表

    Python 数据结构之旋转链表 简介 在进行链表操作时,有时需要旋转链表的一部分,即将链表的最后几个节点移到链表的头部。本文将讲解 Python 实现旋转链表的方法。 方法 我们需要了解两个概念:旋转链表、链表反转。 旋转链表 假设链表为1-2-3-4-5,k=2,将链表后两个节点移动到链表头部,即转化为4-5-1-2-3。 做法如下: 先遍历链表,得出链…

    数据结构 2023年5月17日
    00
  • oracle 数据库学习 基本结构介绍

    Oracle 数据库学习:基本结构介绍攻略 概述 Oracle 数据库是目前世界上使用最为广泛的一种关系型数据库。学习 Oracle 数据库需要具备一定的数据库基础知识,特别是SQL语言的使用,才能更好地理解 Oracle 数据库的基本结构。本攻略将从以下几个方面介绍 Oracle 数据库的基本结构: 数据库系统组成; Oracle 实例; 数据库; 表空间…

    数据结构 2023年5月17日
    00
  • golang优先级队列的实现全过程

    下面是关于”golang优先级队列的实现全过程”的详细讲解。 什么是优先级队列? 优先级队列是一种常用的数据结构,它可以帮我们按照一定规则(即优先级)将元素按照大小排序,并支持快速插入、删除和查询最大或最小的元素等操作。我们可以将优先级队列想象成一个具有优先级的、自动排序的队列,其中优先级高的元素(比如数字大的元素)排在前面,优先级低的元素(比如数字小的元素…

    数据结构 2023年5月17日
    00
  • 数据结构 – 绪论

    01.绪论 1. 概念 1.1 数据结构 数据 Data:信息的载体。能被计算机识别并处理的符号的集合。 数据元素 Data element:数据的基本单位,通常作为一个整体进行考虑和处理。一个数据元素往往由若干数据项组成。数据项是组成数据元素的不可分割的最小单位。 如学生的信息记录就是一个数据元素,它由学号、姓名、性别等组成。 数据对象 Data obje…

    算法与数据结构 2023年4月18日
    00
  • JS中的算法与数据结构之链表(Linked-list)实例详解

    JS中的算法与数据结构之链表(Linked-list)实例详解 什么是链表? 链表是计算机科学中的一种数据结构,由一系列结点(Link,也称为节点)组成,并通过每个节点中的指针(Pointer)链接在一起。每个节点包含数据和一个指向某个位置的引用。 链表的主要特点是在插入和删除操作中表现出很高的效率。与数组相比,链表的访问和操作速度较慢,但在处理动态结构数据…

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