首页 文章资讯内容详情

Golang并发编程

2026-06-01 3 花语

1.Mutex几种状态

1、Mutex几种状态

⚫mutexLocked—表示互斥锁的锁定状态;

⚫mutexWoken—表示从正常模式被从唤醒;

⚫mutexStarving—当前的互斥锁进入饥饿状态;

⚫waitersCount—当前互斥锁上等待的Goroutine个数;

2.Mutex正常模式和饥饿模式

正常模式(非公平锁)

正常模式下,所有等待锁的goroutine按照FIFO(先进先出)顺序等待。唤醒的

goroutine不会直接拥有锁,而是会和新请求锁的goroutine竞争锁的拥有。

新请求锁的goroutine具有优势:它正在CPU上执行,而且可能有好几个,所

以刚刚唤醒的goroutine有很大可能在锁竞争中失败。

在这种情况下,这个被唤醒的goroutine会加入到等待队列的前面。

如果一个等待的goroutine超过1ms没有获取锁,那么它将会把锁转变为饥饿模式。

饥饿模式(公平锁)

为了解决了等待G队列的长尾问题

饥饿模式下,直接由unlock把锁交给等待队列中排在第一位的G(队头),同

时,饥饿模式下,新进来的G不会参与抢锁也不会进入自旋状态,会直接进入

等待队列的尾部,这样很好的解决了老的g一直抢不到锁的场景。

饥饿模式的触发条件,当一个G等待锁时间超过1毫秒时,或者当前队列只剩

下一个g的时候,Mutex切换到饥饿模式。

总结

对于两种模式,正常模式下的性能是最好的,goroutine可以连续多次获取

锁,饥饿模式解决了取锁公平的问题,但是性能会下降,其实是性能和公平的

一个平衡模式。

3Mutex允许自旋的条件

锁已被占用,并且锁不处于饥饿模式。

积累的自旋次数小于最大自旋次数(active_spin=4)。

cpu核数大于1。

有空闲的P。

当前goroutine所挂载的P下,本地待运行队列为空。

4RWMutex实现

通过记录readerCount读锁的数量进行控制,当有一个写锁的时候,会将读锁数量设置为负数

1<<30.目的是让新进入的读锁等待写锁之后释放通知读锁.

同样的写锁也会等等待之前的读锁都释放完毕,才会开始进行后续的操作.

而等写锁释放完之后,会将值重新加上1<<30,并通知刚才新进入的读锁

(rw.readerSem)两者互相限制

5RWMutex注意事项

⚫RWMutex是单写多读锁,该锁可以加多个读锁或者一个写锁

⚫读锁占用的情况下会阻止写,不会阻止读,多个goroutine可以同时获取读锁

⚫写锁会阻止其他goroutine(无论读和写)进来,整个锁由该goroutine独占

⚫适用于读多写少的场景

⚫RWMutex类型变量的零值是一个未锁定状态的互斥锁。

⚫RWMutex在首次被使用之后就不能再被拷贝。

⚫RWMutex的读锁或写锁在未锁定状态,解锁操作都会引发panic。

⚫RWMutex的一个写锁Lock去锁定临界区的共享资源,如果临界区的共享资源已被(读锁或写锁)锁定,这个写锁操作的goroutine将被阻塞直到解锁。

⚫RWMutex的读锁不要用于递归调用,比较容易产生死锁。

⚫RWMutex的锁定状态与特定的goroutine没有关联。一个goroutine可以RLock(Lock),另一个goroutine可以RUnlock(Unlock)。

⚫写锁被解锁后,所有因操作锁定读锁而被阻塞的goroutine会被唤醒,并都可以成功锁定读锁。

⚫读锁被解锁后,在没有被其他读锁锁定的前提下,所有因操作锁定写锁而被阻塞的goroutine,其中等待时间最长的一个goroutine会被唤醒

6、Cond是什么

Cond实现了一种条件变量,可以使用在多个Reader等待共享资源ready的场

景(如果只有一读一写,一个锁或者channel就搞定了)

每个Cond都会关联一个Lock(*sync.Mutexor*sync.RWMutex),当修改条

件或者调用Wait方法时,必须加锁,保护condition

7、Broadcast和Signal区别

func(c*Cond)Broadcast() //Broadcast会唤醒所有等待c的goroutine。 //调用Broadcast的时候,可以加锁,也可以不加锁。 func(c*Cond)Signal() //Signal只唤醒1个等待c的goroutine。 //调用Signal的时候,可以加锁,也可以不加锁。

8、Cond中Wait使用

func(c*Cond)Wait() Wait()会自动释放c.L,并挂起调用者的goroutine。之后恢复执行,Wait()会 在返回时对c.L加锁。 除非被Signal或者Broadcast唤醒,否则Wait()不会返回。 由于Wait()第一次恢复时,C.L并没有加锁,所以当Wait返回时,调用者通常 并不能假设条件为真。 取而代之的是,调用者应该在循环中调用Wait。(简单来说,只要想使用 condition,就必须加锁。) c.L.Lock() for!condition(){ c.Wait() } ...makeuseofcondition... c.L.Unlock()

9、WaitGroup用法

一个WaitGroup对象可以等待一组协程结束。使用方法是:

main协程通过调用wg.Add(deltaint)设置worker协程的个数,然后创建worker协程;

worker协程执行结束以后,都要调用wg.Done();

main协程调用wg.Wait()且被block,直到所有worker协程全部执行结束后返回。

10、WaitGroup实现原理

⚫WaitGroup主要维护了2个计数器,一个是请求计数器v,一个是等待计数

器w,二者组成一个64bit的值,请求计数器占高32bit,等待计数器占低

32bit。

⚫每次Add执行,请求计数器v加1,Done方法执行,请求计数器减1,v为0时通过信号量唤醒Wait()。

11什么是sync.Once

Once可以用来执行且仅仅执行一次动作,常常用于单例对象的初始化场景。 Once常常用来初始化单例资源,或者并发访问只需初始化一次的共享资源,或者在测试的时候初始化一次测试资源。 sync.Once只暴露了一个方法Do,你可以多次调用Do方法,但是只有第一次调用Do方法时f参数才会执行,这里的f是一个无参数无返回值的函数。

12、什么操作叫做原子操作

一个或者多个操作在CPU执行过程中不被中断的特性,称为原子性(atomicity)。这些操作对外表现成一个不可分割的整体,他们要么都执行,要么都不执行,外界不会看到他们只执行到一半的状态。而在现实世界中,CPU不可能不中断的执行一系列操作,但如果我们在执行多个操作时,能让他们的中间状态对外不可见,那我们就可以宣城他们拥有了“不可分割”的原子性。在Go中,一条普通的赋值语句其实不是一个原子操作。列如,在32位机器上写int64类型的变量就会有中间状态,因为他会被拆成两次写操作(MOV)——写低32位和写高32位。

13、原子操作和锁的区别

原子操作由底层硬件支持,而锁则由操作系统的调度器实现。锁应当用来保护一段逻辑,对于一个变量更新的保护,原子操作通常会更有效率,并且更能利用计算机多核的优势,如果要更新的是一个复合对象,则应当使用

atomic.Value封装好的实现。

14、什么是CAS

CAS的全称为CompareAndSwap,直译就是比较交换。是一条CPU的原子指令,其作用是让CPU先进行比较两个值是否相等,然后原子地更新某个位置的值,其实现方式是给予硬件平台的汇编指令,在intel的CPU中,使用的

cmpxchg指令,就是说CAS是靠硬件实现的,从而在硬件层面提升效率。

简述过程是这样:

假设包含3个参数内存位置(V)、预期原值(A)和新值(B)。V表示要更新变量的值,E表示预期值,N表示新值。仅当V值等于E值时,才会将V的值设为N,如果V值和E值不同,则说明已经有其他线程在做更新,则当前线程什么都不做,最后CAS返回当前V的真实值。CAS操作时抱着乐观的态度进行的,它总是认为自己可以成功完成操作。基于这样的原理,CAS操作即使没有锁,也可以发现其他线程对于当前线程的干扰。

15sync.Pool有什么用

对于很多需要重复分配,回收内存的地方,sync.Pool是一个很好的选择.

频繁地分配内存会给GC带来一定的负担,严重的时候会引起CPU的毛刺

而sync.Pool可以将暂时不用的对象缓存起来,待下次需要的时候直接使用

,不用再次经过内存分配,复用对象的内存,减GC压力,提升系统性能

原文链接: