Go Concurrency Learning
一、协程 vs 线程
- 进程与线程面试必备,这里不提了,背景知识还有-些用户态线程、内核态线程以及二者的混合等。以前接触python时也接触到了协程的概念(coroutine) ,因为python用的少没有深入了解,现在学go又遇到了协程(goroutine),这里积累一下。
- 在java中,线程是与操作系统的线程是一对一的,在windows下就是原生的操作系统线程,linux中是轻量级进程(pthread库),通过系统调用,由操作系统来对线程对调度进行管理。虽然线程比进程开销要小,但是java中每一个线程都对应一个内核线程,还是有一定的线程创建、切换的代价的,这个代价可以从分配的内存、寄存器数量、用户态和内核态的转换等方面来看
- 在go中,协程是go实现的一种用户态线程,与内核线程是M:N 多对多的关系
a.协程在用户空间中,创建切换的代价比系统线程要小,因为内核空间对协程是不感知的,协程的调度切换无需依赖内核,调度是发生在用户态的
b. goroutine启动时默认栈大小只有2k,栈大小可以动态扩容,也可以动态收缩i. Split stacks机制,可以用java中ArrayList扩容机制来理解。预先分配一个一定大小的stack,栈空间不够了就开辟一个新的,然后把旧的copy到新的上去。 ii. go中使用了**连续栈**,新的栈空间和旧的是连续的
- goroutine工作原理:
a. goroutine建立在操作系统线程基础之上,它与操作系统线程之间实现了一个多对多的线程模型(M:N)。即M的协程对应N个操作系统线程,操作系统内核调度N个操作系统线程,这N个操作系统线程又负责调度M个协程。这个调度可以理解为按照一定的策略算法在某个时候选择某个goroutine分配cpu时间片然后运行该goroutine的过程
二、并发编程模型
- 以前在java中,并发模式主要是多线程共享内存,线程之间的通信可以通过基本的wait、notify实现,或者是进阶一点的依赖AQS的操作,比如ReetranLock的Condition,CyclicBarrier这些。总的来说java的多线程并发还是依赖于锁和JUC包下的一些类的
- go的并发模型: CSP, communicating sequential processes
a. 不是通过共享内存来通信,而是通过通信来共享内存
Q: goroutine与内核线程是如何实现多对多的?由此引出MPG模式
go使用MPG模式来实现CSP
a. M: Machine, 一个M对应一个内核线程,可以理解为工作线程, M维护了当前执行的G协程,以及其他一些信息, M的作用就是执行G中包装的并发任务
b. P: Process,代表M所需的上下文Context,维护了一个runnable gorouines队列,可以把p理解为局部调度器i. 下图是type struct p的一些属性,这些表示runnable goroutines的队列 ii. P的值对应于环境变量GOMAXPROCS的值,可以在运行时修改,可以理解为用多少个核来调度这些协程
c. G:Goroutine,协程,可以理解为程序中一段go代码,维护了栈,指令指针等
d. schedt:在用户空间中实现的调度器,维护存储M和G的队列,以及调度器的一些状态信息等。调度器的目的就是将G公平合理的安排到多个M上去执行。注:这里其实有两个存储goroutine的队列,一个是P(局部调度器)的local queue,一个是全局调度器数据模型schedt的global queue
看源码:src/runtime/runtime2.go
对MPG的理解
a. 每个M都对应着一个内核线程,M和内核线程是1对1的
b. M和P互相依赖,M和P之间也是1对1的,最多有MAXPROCS个P,可能有很多个M,只有绑定了M的P才能运行,因此可以说P才是真正的并行单元
c. P和G之间是一对多的关系
d. 至此可以总结到,通过MPG模式可以实现goroutine和内核线程多对多的关系,提高并发效率从创建go进程(go run xxx.go),到执行go写成,到程序终止退出,这个过程中的细节【FIXME】
// 协程调度的细节,待了解【FIXME】
这块感觉可以和java线程池来对比(核心线程数、最大线程数、阻塞队列、任务拒绝策略等)
三、Go内存模型
java有JMM,其中涉及到内存屏障、重排序、happens-before原则等,现在来理解一下Go中的内存模型
reference:https://golang.google.cn/ref/mem
1)go内存模型简介
go的内存模型想要达成以下效果:在一个协程中修改一个变量,在另外一个协程中读取这个变量时,需要保证读取到修改后的值
类比:java volatile
2)go happens-before原则
在单个goroutine内,代码的顺序就是happens before的顺序。
理解:这句话不是说但个协程内不可以重排序,应该意思是可以做重排序优化,只要不影响到最终结果,优化的关键在于分析数据依赖性(例如Spark中建立DAG来分析RDD之间的依赖关系)init函数
a. 如果包P导入了包Q,那么Q的init函数 happens before P中的所有操作
b. main函数 happens after 所有的init函数Goroutine
a. Goroutine的创建 happens before 该Goroutine中的所有操作
b. Goroutine的销毁 happens after 该Goroutine中的所有操作Channel
a. send操作 happens before对应的receive完成操作
b. 对channel的close操作heppens before receive端收到关闭通知(收到0值)的操作
c. 有buffer的channel: 向channel 中写数据 happens before 从channel中读取数据 【带buffer的channel是先写后读】
d. 没有buffer的channel: 从channel 中读取数据happens before 向channel中写入数据【先读后写】
e. 规则抽象,k+C, 这个暂时不管,以后应该会理解Lock
a. Go中lbock有Mutex和RWMutex两种,RWMutex可以对应jave中的读写锁,ReenurentReaswriteLock,分离了读锁和写锁
b. Go中RWMutex的底层实现机制待了解,是否可重入。可抢占(非公平)待了解
c. 上一次的 unlock happens before这一次的lock
d. 对于读写锁而言,RLock happens after 上一次的UnLock,其对应的RUnlock happens before下一次的LockOnce
a. 多个goroutine下同时调用once.Do(f)时候,真正执行f()函数的goroutine,happen before任何其他由于调用once.Do(f)而被阻塞的goroutine返回
b. Once可以实现单例模式
c. Once的常见应用模式,待了解
3)不正确的同步
// TODO
4)连续栈 vs 分段栈
// TODO
四、Go垃圾回收机制
- Go1.5之前,采用标记清除法。缺点: stop the world时间太长,对于服务端高并发场景非常不友好
- Go1.5之后,三色标记法 + 写屏障 + 辅助GC,三色标记法是标记清除法的一种增强版本
三色标记法:
1.对象标记三种颜色(白色,灰色,黑色),GC开始时所有对象默认是白色,标记完成后可达的对象都会被标记为黑色,灰色是一种中间状态
Golang中可以作为GC Root的有什么? 【待了解,TODO】
原理步骤:
a. 初始,所有对象为白色
b. 从gc root出发扫描所有对象,将它们引用的对象标记为灰色
C. 分析灰色对象是否引用了其他对象,如果没有引用其它对象则将该灰色对象标记为黑色,如果有引用则将它变为黑色的同时将它引用的对象也变为灰色
d. 重复步骤3,直到灰色对象队列为空
e. 此时白色对象即为垃圾,可进行回收理解:三色标记法的目标可以和java gc中的CMS收集器类比,都是希望在标记和清除这两个耗时时间较长的过程中,gc线程可以和用户线程一起工作,处于一种并发状态,降低停顿时间
写屏障
- 需要写屏障的原因:在标记阶段,gc跟用户程序是并发跑的,那么有可能用户程序会一直修改内存,gc可能会漏标记对象,因此需要写屏障
- 标记阶段除了并发标记之外还需要一个re-scan阶段,理由如上,感觉也跟java CMS收集器中的 修正标记阶段 类似,这个阶段是需要stop the world的
- Go1.8 之后采用混合的写屏障模式来避免re-scan Proposal: Eliminate STW stack re-scanning
辅助GC
- 辅助gc这个不是很理解,意思是把一部分的标记和清除工作交给用户协程来执行吗?
GC触发条件
- 超过内存大小阈值 gcTriggerHeap,下次内存达到这次gc时内存大小的2倍就开始回收
- 达到定时时间:gcTriggerTime,一直达不到两杯,则gc会定时触发
- 手动触发gc:gcTriggerCycle
源码:runtime/mgc.go1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16const (
// gcTriggerHeap indicates that a cycle should be started when
// the heap size reaches the trigger heap size computed by the
// controller.
gcTriggerHeap gcTriggerKind = iota
// gcTriggerTime indicates that a cycle should be started when
// it's been more than forcegcperiod nanoseconds since the
// previous GC cycle.
gcTriggerTime
// gcTriggerCycle indicates that a cycle should be started if
// we have not yet started cycle number gcTrigger.n (relative
// to work.cycles).
gcTriggerCycle
)
GC调优
reference:
五、Go Context的使用
- https://blog.golang.org/context 国内翻译版:https://learnku.com/docs/go-blog/context/6545
- https://www.flysnow.org/2017/05/12/go-in-action-go-context.html
- https://draveness.me/golang/docs/part3-runtime/ch06-concurrency/golang-context