您现在的位置是:首页 > 技术教程 正文

Go面试题:锁的实现原理sync-mutex篇

admin 阅读: 2024-03-18
后台-插件-广告管理-内容页头部广告(手机)

在Go中,主要实现了两种锁:sync.Mutex(互斥锁) 以及 sync.RWMutex(读写锁)。

本篇主要给大家介绍sync.Mutex的使用和实现原理。

文章目录

    • 为什么需要锁
    • 在Go中对于并发程序进行公共资源的访问的限制最常用的就是互斥锁(sync.mutex)的方式
    • 实现原理
    • 锁的两种模式
    • 注意事项

为什么需要锁

在高并发下或多goroutine同时执行下,可能会同时读写同一块内存,比如如下场景:

var count int var mu sync.Mutex func func1() { for i := 0; i < 1000; i++ { go func() { count = count + 1 }() } time.Sleep(time.Second) fmt.Println(count) }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

输出的值预期是1000,实际是 948,965等,多次运行结果不一致。
之所以出现这样的现象,是因为对于count=count+1来讲,每个goroutine执行步骤为:

  • 读取当前count值
  • count+1
  • 修改count值

当多个goroutine同时执行修改数值时,后面执行的goroutine会把前面goroutine对count的修改覆盖。

在Go中对于并发程序进行公共资源的访问的限制最常用的就是互斥锁(sync.mutex)的方式

sync.mutex的常用方法有两个:

  • Mutex.lock()用来获取锁
  • Mutex.Unlock()用于释放锁
    在 Lock 和 Unlock 方法之间的代码段称为资源的临界区,这一区间的代码是严格被锁保护的,是线程安全的,任何一个时间点最多只能有一个goroutine在执行。

基于此,上面的示例可以采用sync.mutex来改进:

var count int var mutex sync.Mutex func func2() { for i := 0; i < 1000; i++ { go func() { mutex.Lock() count = count + 1 mutex.Unlock() }() } time.Sleep(time.Second) fmt.Println(count) }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

输出结果为1000。

当某一goroutine执行了mutex.lock()方法后,如果有其他的goroutine来执行上锁操作,会被阻塞,直到当前的goroutine执行mutex.unlock()方法释放锁后其他的goroutine才会继续抢锁执行。

实现原理

sync.Mutex的数据结构
Go中的sync.Mutex的结构体为:

type Mutex struct { state int32 sema uint32 }
  • 1
  • 2
  • 3
  • 4

Sync.Mutex由两个字段构成,state用来表示当前互斥锁处于的状态,sema用于控制锁状态的信号量。相信各位道友读完这两个字段的描述后,好像懂了,又好像没懂。下面我们详细理解下这两个字段到底都作了哪些事。
互斥锁state主要记录了如下四种状态:

waiter_num: 记录了当前等待抢这个锁的goroutine数量
starving: 当前锁是否处于饥饿状态 (后文会详解锁的饥饿状态) 0: 正常状态 1: 饥饿状态
woken: 当前锁是否有goroutine已被唤醒。 0:没有goroutine被唤醒; 1: 有goroutine正在加锁过程
locked: 当前锁是否被goroutine持有。 0: 未被持有 1: 已被持有
sema信号量的作用
当持有锁的gorouine释放锁后,会释放sema信号量,这个信号量会唤醒之前抢锁阻塞的gorouine来获取锁。

锁的两种模式

互斥锁在设计上主要有两种模式: 正常模式和饥饿模式。

之所以引入了饥饿模式,是为了保证goroutine获取互斥锁的公平性。所谓公平性,其实就是多个goroutine在获取锁时,goroutine获取锁的顺序,和请求锁的顺序一致,则为公平。
正常模式下,所有阻塞在等待队列中的goroutine会按顺序进行锁获取,当唤醒一个等待队列中的goroutine时,此goroutine并不会直接获取到锁,而是会和新请求锁的goroutine竞争。 通常新请求锁的goroutine更容易获取锁,这是因为新请求锁的goroutine正在占用cpu片执行,大概率可以直接执行到获取到锁的逻辑。

饥饿模式下, 新请求锁的goroutine不会进行锁获取,而是加入到队列尾部阻塞等待获取锁。
饥饿模式的触发条件

  • 当一个goroutine等待锁的时间超过1ms时,互斥锁会切换到饥饿模式

饥饿模式的取消条件:

  • 当获取到锁的这个goroutine是等待锁队列中的最后一个goroutine,互斥锁会切换到正常模式
  • 当获取到锁的这个goroutine的等待时间在1ms之内,互斥锁会切换到正常模式

注意事项

  1. 在一个goroutine中执行Lock()加锁成功后,不要再重复进行加锁,否则会panic。
  2. 在Lock() 之前 执行Unlock()释放锁 会panic
  3. 对于同一把锁,可以在一个goroutine中执行Lock加锁成功后,可以在另外一个gorouine中执行Unlock释放锁。
标签:
声明

1.本站遵循行业规范,任何转载的稿件都会明确标注作者和来源;2.本站的原创文章,请转载时务必注明文章作者和来源,不尊重原创的行为我们将追究责任;3.作者投稿可能会经我们编辑修改或补充。

在线投稿:投稿 站长QQ:1888636

后台-插件-广告管理-内容页尾部广告(手机)
关注我们

扫一扫关注我们,了解最新精彩内容

搜索