1.go并发简介

goroutine 是一种非常轻量级的实现,可在单个进程里执行成千上万的并发任务,它是Go语言并发设计的核心。
goroutine比线程更小,十几个 goroutine 可能体现在底层就是五六个线程,而且Go语言内部也实现了 goroutine 之间的内存共享。
使用 go 关键字就可以创建 goroutine,将 go 声明放到一个需调用的函数之前,在相同地址空间调用运行这个函数,这样该函数执行时便会作为一个独立的并发线程

1.GMP模型

image.png
G:goroutine是 Go 语言调度器中待执行的任务,它在运行时调度器中的地位与线程在操作系统中差不多,但是它占用了更小的内存空间,也降低了上下文切换的开销
M:machine,可以理解为线程
P:processor即处理器。处理器 P 是线程和 Goroutine 的中间层,它能提供线程需要的上下文环境,也会负责调度线程上的等待队列。只有在M和P进行关联之后才可以执行G,通过处理器 P 的调度,每一个内核线程都能够执行多个 Goroutine,它能在 Goroutine 进行一些 I/O 操作时及时让出计算资源,提高线程的利用率

一个G被创建之后,会被加载到本地队列之中。如果本地队列已满,则会连着一半的本地队列的G一起被加入到全局队列当中。如果本地队列的G被全部执行完,那么P会优先从全局队列取一半的G,如果全局队列也是空的,那么将从旁边的P中偷取一半的G

调度场景
Channel阻塞:当goroutine读写channel发生阻塞时候,会调用gopark函数,该G会脱离当前的M与P,调度器会执行schedule函数调度新的G到当前M。
系统调用:当某个G由于系统调用陷入内核态时,该P就会脱离当前的M,此时P会更新自己的状态为Psyscall,M与G互相绑定,进行系统调用。结束以后若该P状态还是Psyscall,则直接关联该M和G,否则使用闲置的处理器处理该G。
系统监控:当某个G在P上运行的时间超过10ms时候,或者P处于Psyscall状态过长等情况就会调用retake函数,触发新的调度。
主动让出:由于是协作式调度,该G会主动让出当前的P,更新状态为Grunnable,该P会调度队列中的G运行。

2.一个有趣的问题

image.png
运行结果:0到4的乱序输出
image.png
运行结果:4,0,1,2,3
原因:在启用一个线程的时候,G会按照固定的顺序执行。新创建的G会被放到P的runnext中,runnext的旧值会被添加到runq队列中去。新建一个G的时候next固定为true。next为false的情况只有两种,一是从全局队列中获取G到本地时,二是 gc的goroutine被创建的时候(一般由go运行时自主创建)所以当最后一个G,也就是打印4这个G被创建出来后,P的runnext的输出是4,而P的本地队列中G的输出依次是0,1,2,3,执行G的时候先执行runnext,再依次执行队列,输出结果就是4,0,1,2,3
image.png
运行结果:3,4,0,1,2
原因:G执行的顺序依旧为4,0,1,2,3,但是执行每一个的时候都会执行到time.Sleep进行休眠,然后休眠时间结束之后会调用ready去重新添加到本地队列,添加操作的顺序也是4 0 1 2 3(与休眠的顺序相对应),但是此时由于传入的next值为true,所以会依次对P的runnext进行更改,最终第一个sleep的”打印4“唤醒操作执行完之后,P的runnext为3,runq为4 0 1 2,所以最终的输出结果就是3 4 0 1 2

3.上下文context

上下文是Go 语言中用来设置截止日期、同步信号,传递请求相关值的结构体。上下文与 Goroutine 有比较密切的关系。每一个context都会从最顶层的 Goroutine 一层一层传递到最下层。它可以在上层 Goroutine 执行出现错误时,将信号及时同步给下层。

2.并发编程

1.启动并发编程

image.png
真个协程池的启动也是在主协程当中的,所以会先打印“Game start”,再执行启动的每个G,最后打印“Game over”

2.协程安全的方法

1.互斥锁sync.Mutex{}

sync.Mutex是一个不可重入的锁,大部分情况下会产生死锁。对一个没有lock或者已经unlock的Mutex进行解锁会产生panic,推荐实践中使用defer立即进行unlock
为了防止每个goroutine因为获取不到锁导致无法进行,mutex会将goroutine排成队列,优先交给等待队列最前面的 Goroutine。新的 Goroutine 在该状态下不能获取锁、也不会进入自旋状态,它们只会在队列的末尾等待

2.读写互斥锁sync.RwMutex{}

读写互斥锁是细粒度的互斥锁,它不限制资源的并发读,但是读写、写写操作无法并行执行
一般用于读多写少的场景

3.sync.Once{}

程序运行期间的某段代码只会执行一次,通常用来实现单例模式

  1. type singleton struct{}
  2. var ins *singleton
  3. var once sync.Once
  4. func GetIns() *singleton {
  5. once.Do(func(){
  6. ins = &singleton{}
  7. })
  8. return ins
  9. }

4.sync.Map{}

image.png

5.channel

不通过共享内存的方式进行通信,而是通过通信的方式共享内存,于是诞生了channel
ch := make(chan int,100) //100代表容量,用于缓冲区
ch <- 5 //发送
i ,ok := <- ch //接受
defer close(ch) //关闭

select的搭配:同时有多个channel可以接收数据,那么Go会伪随机的选择一个case处理(pseudo-random)。如果没有case需要处理,则会选择default去处理,如果default case存在的情况下。如果没有default case,则select语句会阻塞,直到某个case需要处理(nil会造成持续堵塞)

参考资料

记一次关于goroutine调度的探索 https://blog.csdn.net/a12139132/article/details/101208531
当runtime.GOMAXPROCS(1)时多个协程的执行顺序 https://www.jianshu.com/p/888f8b03de91
Golang调度器的GMP模型 https://zhuanlan.zhihu.com/p/261590663