并发

go协程

线程池有效的减少线程创建和销毁所带来的开销。若 worker 线程执行的 G 任务中发生系统调用,则操作系统会将该线程置为阻塞状态,浪费线程资源,线程池消费任务队列的能力变弱了。增加线程池中线程数量可以一定程度上提高消费能力,但随着线程数量增多,过多线程会争抢 CPU,线程数过多,那么操作系统会频繁的切换线程,频繁的上下文切换就成了性能瓶颈。

CSP并发机制

以通信的⽅式来共享内存。Golang内部有三个对象:

每⼀个线程(M0)维护⼀个上下⽂(P),任何时刻,⼀个上下⽂中只有⼀个Goroutine,其他Goroutine在上下文对应的runqueue中等待。

队列轮转:每个 P有个局部队列,局部队列保存待执⾏的 goroutine(流程2),当 M绑定的 P的的局部队列已经满了之后就会把 goroutine 放到全局队列(流程2-1)。P 会周期性的将G调度到M中执行,执行一段时间后,保存上下文,将G放到队列尾部,然后从队列中再取出一个G进行调度。当 M绑定的 P的局部队列为空时,M会从全局队列获取到本地队列来执⾏,全局队列中 G 的来源,主要有从系统调用中恢复的 G,防止全局队列中的 G 被“饿死”。

工作窃取:多个 P 中维护的 G 队列有可能是不均衡的。当从全局队列中没有获取到可执⾏的 G时候,M会从其他 P 的局部队列中偷取 G来执⾏(流程3.2)。确保了每个 OS 线程都能充分的使用

image-20220816104936249

系统调用:一般 M 的个数会略大于 P 的个数,多出来的 M 会在 G 产生系统调用时发挥作用。当G0即将进入系统调用时,M0将释放P,进而某个空闲的M1获取P,继续执行P队列中剩下的G。当G0系统调用结束后,如果有空闲的P,则获取一个P,继续执行G0。如果没有,则将G0放入全局队列,等待被其他的P调度,然后M0将进入缓存池睡眠。

阻塞:当 G因 channel 或者 network I/O 阻塞时,不会阻塞 M,M会寻找其他 runnable 的 G;当阻塞的 G恢复后会重新进⼊ runnable 进⼊ P队列等待执⾏。

image-20220812225206078

image-20220812221636833

线程实现,m内核线程:n用户线程

Mutex

正常模式(⾮公平锁):正常模式下,所有等待锁的 goroutine 按照 FIFO(先进先出)顺序等待。唤醒的 goroutine 不会直接拥有锁,⽽是会和新请求锁的 goroutine 竞争锁的拥有。新请求锁的 goroutine 具有优势:它正在 CPU上执⾏,⽽且可能有好⼏个,所以刚刚唤醒的 goroutine 有很⼤可能在锁竞争中失败。如果⼀个等待的 goroutine 超过1ms没有获取锁,那么它将会把锁转变为饥饿模式。

饥饿模式(公平锁):为了解决了等待 G队列的⻓尾问题,饥饿模式下,直接由 unlock 把锁交给等待队列中排在第⼀位的 G(队头),同时,饥饿模式下,新进来的 G不会参与抢锁也不会进⼊⾃旋状态,会直接进⼊等待队列的尾部,这样很好的解决了⽼的 g ⼀直抢不到锁的场景。

对于两种模式,正常模式下的性能是最好的,goroutine 可以连续多次获取锁,免去上下文切换开销,饥饿模式解决了取锁公平的问题,但是性能会下降,其实是性能和公平的⼀个平衡模式。

sync.Mutex互斥锁,使同一时刻只能有一个协程执行某段程序,其他协程等待该协程执行完再依次执行。

 

RWMutex

RWMutex 是单写多读锁,适⽤于读多写少的场景,通过记录 readerCount 读锁的数量来进⾏控制,当有⼀个写锁的时候,会将读锁数量设置为负数1<<30。⽬的是让新进⼊的读锁等待写锁释放之后再获取读锁。同样的写锁也会等待之前的读锁都释放完毕,才会开始进⾏后续的操作。

写锁释放完之后,会将值重新加上1<<30,并通知刚才新进⼊的读锁(rw.readerSem),所有因操作锁定读锁⽽被阻塞的 goroutine 会被唤醒,并都可以成功锁定读锁。读锁被解锁后,在没有被其他读锁锁定的前提下,所有因操作锁定写锁⽽被阻塞的 goroutine,其中等待时间最⻓的⼀个 goroutine 会被唤醒。

sync.RWMutex,写所互斥,读锁不互斥

WaitGroup

⼀个 WaitGroup 对象可以等待⼀组协程结束。调⽤ wg.Add(delta int)设置 worker 协程的个数,然后创建 worker 协程;worker 协程执⾏结束以后,都要调⽤ wg.Done();main 协程调⽤ wg.Wait()且被 block,直到所有 worker 协程全部执⾏结束后返回。

WaitGroup 主要维护了2 个计数器,⼀个是请求计数器 v,⼀个是等待计数器 w,⼆者组成⼀个64bit 的值,请求计数器占⾼32bit,等待计数器占低32bit。每次 Add执⾏,请求计数器 v 加1,Done⽅法执⾏,请求计数器减1,v 为0 时通过信号量唤醒 Wait()。

sync.WaitGroup等待多个任务执行完毕

Context 包

1,Context包提供上下文机制在 goroutine之间传递 deadline、取消信号(cancellation signals)或者其他请求相关的信息。

once

sync.Once在高并发的场景下,来保证代码只执行一次, 适合用于创建单例、只加载一次资源等只需要执行一次的场景。

启动时没有任何go程被执行完毕,标志位为0,多个go程竞争一个同步锁,竞争成功的go程获得同步锁,其它线程阻塞,在他执行完毕后将执行标志位写为1并释放锁,其他go程再次开始竞争锁,拿到锁后检查标志位,发现标志位为1,直接返回并释放锁。

Cond

条件变量 sync.Cond,基于互斥锁的基础上,增加了一个通知队列,协程刚开始是等待的,通知的协程会从通知队列中唤醒一个或多个被通知的协程。

select

select 语句使一个 Go 程可以等待多个通信操作。每个case表达式中都必须包含通道的读或者写;select语句会查看哪些case的读写操作能成功执行,然后开始选择能成功执行的候选分支,进行读写操作,执行对应case内容,然后结束当前select ;当多个分支都准备好时会随机选择一个执行case,而随机的引入就是为了避免饥饿问题的发生,然后结束当前select 。如果所有的候选分支都不满足选择条件,那么默认分支就会被执行,如果这时没有默认分支,那么select语句就会立即进入阻塞状态,直到至少有一个候选分支满足选择条件为止。

并发限制
函数传参

因为拷贝的内容有时候是非引用类型(int、string、struct等这些),这样就在函数中就无法修改原内容数据;有的是引用类型(指针、map、slice、chan等这些),这样就可以修改原内容数据。