golang之互斥锁mutex与读写锁


锁是为了避免竞争而建立的并发控制手段,为有序地访问共享资源。

互斥锁mutex

Mutex为一结构体类型,对外暴露Lock与Unlock接口。加锁与解锁要成对出现(应加锁后,立即用defer解锁),重复解锁会引起panic

Mutex内存布局:
Mutex内存布局

Mutex有以下状态:

  • Locked:是否已被锁定(0:没锁定,1:锁定);
  • Woken:是否有协程已被唤醒,正处于加锁状态(0:无协程唤醒,1:有协程唤醒);
  • Starving:是否处于饥饿状态(0:没有饥饿,1:饥饿状态);
  • Waiter:阻塞的等待锁的协程数(解锁时据此判断是否要释放信号量);

加解锁

协程间抢锁实际上是抢给Locked赋值的权利(能给Locked域置1,说明抢锁成功);抢不到的阻塞等待Mutex.sema信号量。

Woken状态用于加锁和解锁过程的通信;处于自旋状态的加锁协程会把Woken标记为1,通知解锁协程不必释放信号量了。

加锁、唤醒示意图(最基本的情形):
加解锁示意图

正常模式下,被阻塞的协程会进入等待队列;当持有锁协程释放锁时,会释放唤醒信号来唤醒等待的协程。

自旋

自旋对应于CPU的‘PAUSE’指令(CPU空转),不同于sleep,其不需要把协程转为睡眠状态;加锁时程序会自动判断是否可自旋,自旋必须满足(要不忙):

  • 自旋次数要足够小(通常最多不超4次);
  • CPU核数要大于1(否则自旋无意义);
  • 协程调度机制中的Process数量要大于1;
  • 协程调度机制中可运行队列必须为空;

自旋优势是更充分利用CPU,尽量避免协程切换。若自旋过程中获得锁,那么之前被阻塞协程将无法获得锁,从而可能会进入饥饿状态;为避免协程长时间无法获取锁,自1.8版本后,Mutex增加了Starving状态,此状态下不会自旋(释放锁时,一定会唤醒一个协程并让其成功加锁)。

加锁模式

每个Mutex都有两个模式,称为Normal和Starving:

  • Normal模式:若加锁不成功,不会立即转入阻塞队列,而是判断是否满足自旋条件;
  • Starvation模式:处于饥饿模式下,不会启动自旋过程;一旦有协程释放了锁,那么一定会唤醒协程,被唤醒的协程将会成功获取锁,同时也会把等待计数减1。

释放锁时如果发现有阻塞等待的协程,还会释放一个信号量来唤醒一个等待协程,被唤醒的协程得到CPU后开始运行,此时发现锁已被抢占了,自己只好再次阻塞,不过阻塞前会判断自上次阻塞到本次阻塞经过了多长时间,如果超过1ms的话,会将Mutex标记为”饥饿”模式,然后再阻塞。

基本使用

sync包中提供了锁相关的一系列同步原语,用于加解锁:

import (
	"fmt"
	"sync"
)

func ShowMutex() {
	var syn sync.Mutex
	var count = 0

	var wg sync.WaitGroup
	wg.Add(10)

	for i := 0; i < 10; i++ {
		go func() {
			defer wg.Done()
			for j := 0; j < 100000; j++ {
				syn.Lock()
				count++
				syn.Unlock()
			}
		}()
	}

	wg.Wait()
	fmt.Println(count)
}

读写锁rwmutex

读写互斥锁,可增加并发能力(程序中一般是读多、写少):

  • 写锁阻塞其他写锁;
  • 写锁阻塞读锁;
  • 读锁阻塞写锁;
  • 读锁不阻塞读锁;

接口

RWMutex提供了四个接口:

  • RLock(读锁定):增加读操作计数,(若有写操作)阻塞等待写操作结束;
  • RUnlock(读解锁):减少读操作计数,(最后一个读操作、且有写锁定)唤醒等待等待写操作的协程;
  • Lock(写锁定):获取互斥锁,(若有读操作)等待所有读操作结束;
  • Unlock(写解锁):唤醒因读锁定而被阻塞的协程(若有),解除互斥锁;

读写锁定义:

type RWMutex struct {
	w Mutex //用于控制多个写锁, 获得写锁首先要获取该锁, 如果有一个写锁在进行, 那么再到来的写锁将会阻塞于此
	writerSem uint32 //写阻塞等待的信号量, 最后一个读者释放锁时会释放信号量
	readerSem uint32 //读阻塞的协程等待的信号量, 持有写锁的协程释放锁后会释放信号量
	readerCount int32 //记录读者个数
	readerWait int32 //记录写阻塞时读者个数
}

互斥

写阻塞读

写操作是如何阻止读操作的:

  • readerCount是个整型值,用于表示读者数量,不考虑写操作的情况下,每次读锁定将该值+1,每次解除读锁定将该值-1,所以readerCount取值为[0,N](N为读者个数,最大可支持 2 30 2^{30} 230个并发读者)。
  • 当写锁定进行时,会先将readerCount减去 2 30 2^{30} 230,从而readerCount变成了负值,此时再有读锁定到来时检测到readerCount为负值,便知道有写操作在进行,只好阻塞等待。
  • 真实的读操作个数并不会丢失,只需要将readerCount加上 2 30 2^{30} 230即可获得。

读阻塞写

读操作是如何阻止写操作的:

  • 读锁定会先将readerCount加1,此时写操作到来时发现读者数量不为0,会阻塞等待所有读操作结束。

避免饿死

为什么写锁定不会被饿死:

  • 写操作到来时,会把readerCount值拷贝到readerWait中,用于标记排在写操作前面的读者
    个数。
  • 读操作结束后,除了会递减readerCount,还会递减readerWait值,当readerWait值变为0时唤醒写操作。
Golangmutex互斥锁)是用来保护共享资源的一种机制。当多个goroutine同时访问一个共享资源时,可能会导致数据竞争(data race),因此需要使用mutex来进行同步。下面以一个简单的例子来说明mutex的使用。 假设有一个共享的计数器counter,多个goroutine同时对其进行访问和修改。若不使用mutex进行同步,则可能会导致counter的值出现错误。 在Golang中,我们可以使用sync包中的Mutex类型来创建互斥锁。首先,我们需要创建一个Mutex对象: ``` var mutex sync.Mutex ``` 然后,在对counter进行读写操作之前,我们需要先锁定mutex,这样其他goroutine就无法同时访问counter了: ``` mutex.Lock() ``` 在操作完成后,我们需要解锁mutex,以允许其他goroutine继续访问counter: ``` mutex.Unlock() ``` 下面是一个简单的例子: ```go package main import ( "fmt" "sync" "time" ) var counter int var mutex sync.Mutex func increment() { mutex.Lock() defer mutex.Unlock() counter++ fmt.Println("Counter:", counter) } func main() { for i := 0; i < 10; i++ { go increment() } time.Sleep(time.Second) // 等待goroutine完成 fmt.Println("Final Counter:", counter) } ``` 运行以上代码,我们可以看到每个goroutine对counter进行了递增操作,并且经过互斥锁的保护,最终得到了正确的计数结果。 通过使用mutex,我们可以确保共享资源的安全访问,避免了数据竞争的问题。但是需要注意,过多地使用mutex可能会导致性能下降,因此应该根据实际情况来决定是否需要使用互斥锁
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值