记杂||go的协程到GMP到CSP到context和锁
本文类似于个人学习笔记不会面面俱到,需要读者掌握一定基本知识。
线程是轻量级的进程,而协程是轻量级的线程。进程上下文切换开销>线程(1~2微秒)>协程(0.2微秒),以java为例,每个线程都有程序计数器,虚拟机栈,本地方法栈(默认大小为1MB,创建时指定运行时不可更改);而go的协程也有栈默认大小仅为2KB,并且运行时可以进行动态的扩容,仅从内存大小便可以看出协程的数量可以比进程大得多。
GMP模型
G指Goroutine既go的协程,M指线程(原生线程),P指逻辑处理器。可以抽象理解为G通过P在M上得到运行。同时执行的g<=p(默认为cpu数量)<=m(发生阻塞时可能新建,一般与p相等)
先讲一下g0协程,g0是一种特殊的协程,每个M都有一个g0,主要作用是执行协程调度的一系列代码。g使用协作式+强占式调度,通常g运行结束后会主动退出调度,如果运行时间过长>10ms便会触发强占式调度强制让g退出调度,与m进行解绑,这时便进入到调度循环中(类似上下文切换),用户协程g切换到g0,g0便负责执行3个函数,schedule(处理具体的调度策略,选择下一个执行的g),execute(状态转移,绑定g和m),gogo(与操作系统有关函数),之后便重新切换到新的g上进行运行。
重点讲schedule函数:如何找到下一个g,这便是P的主要作用,首先P中有一个runnext字段指向下一个要执行的协程,新创建的g都会抢先放到这里,若为空,则去P内部长度为256的数组中寻找g,若仍为空,则去不限大小的全局队列中获取,通常会把(全局队列中g / P数量,最多不能超过128)的g转移到p的本地队列中,若全局队列仍未空,则去其他P中拿取一半的g放到自己的队列中。注意每61次调度便会优先在全局队列中寻找g,防止过度饥饿。如果p的本地队列满了,则会拿取一半 128个g放到全局队列中。
并且,如果在M执行G时发生了文件IO导致阻塞等,P便会和M进行解绑,重新选择可以的M或者新建,然后执行P中剩余的g,直到g的阻塞结束,注意go对网络IO进行了异步处理,并不会导致M阻塞,仅阻塞G,M便不会和P解绑,而是去执行P的其他g,这么做的好处是如果线程阻塞会涉及到用户态和内核态的切换过程耗时较长,反观java,由于没做这种用户线程和原生线程的分离式设计,遇到线程相关操作便可能会进行内核态切换,而java的抢占式调度又让切换变得更加频繁。
g除了主动调度,抢占式调度外,在io或channel等阻塞时会进行被动调度,p会去寻找下一个g执行。
CSP
CSP的思想及通过通信来共享内存,如果你了解java可以发现java主要是通过共享内存的方式来通信,如果发生并发安全问题则通过锁等机制来解决。这俩个的区别可以理解为:go中协程a拥有资源A,而协程b想要获取这个资源便在a和b之间架起通道channel,然后a通过channel将A发送给b;而在java中,a和b会共同使用内存中的资源A,谁想用便去访问,如果同时需要则进行加锁。线程间的通信方式不只有这两种,java还有wait/notify等方式,而go也可以使用共享内存的方式。
channel的使用为基础知识,这里不进行介绍,简单介绍select:select可以帮助我们同时监控多个通道,若同时面对多个可执行通道,select会随机选取一个来执行。
通道底层为一个hchan结构体,有2个队列,读取和写入的阻塞协程队列,正常情况下一定有一个队列为空,既如果有一个g1阻塞在读队列中,那么写的g2便直接将数据传递给g1。如果为通道设置了缓冲区,则底层会创建一个设定类型的数组并实现了一个环形队列,如果缓冲区未满,写入的g便会直接把数据写入到缓冲区中不会进行阻塞。注意,g如果进入阻塞队列也涉及到m和g解绑,m执行p中其他的g的过程。
context
如果你不知道如何退出一个协程,那么就不要创建这个协程。
context的主要作用便是可以级联终止,当父context退出时,使用子context便都会退出,而子context的退出不会影响到父context。通过context我们可以设置超时时间来更好的管理协程,同时他也内置了一个kv键值对,可以用来存放数据,但使用时需要谨慎,子context可以通过k得到父的v但是由于是一层一层的向上查找,查询速度较慢。
锁
go的协程可以进行通过共享内存来进行通讯,同java为了保证协程安全同样需要锁的机制。go主要有2种锁:互斥锁和读写锁,同时也提供了原子操作的实现方式。
面对指令重排问题,go没有提供volatile但是提供了一系列原子操作,通过其中提供的cas操作+自旋锁便可以达到原子操作的目标,还可以实现信号量等。
go的互斥锁和读写锁都是信号量的方式来判断是直接获取锁还是进行等待,如果抢锁失败便会进行一段时间的自旋操作,若仍未获取便根据信号量来判断是否需要陷入休眠。go的读写锁遵循着读读不互斥,其余互斥的原则,如果申请写锁会等所有读锁都释放后再进行操作,反之亦然。
Lotalot你干了什么?!没有golang八股文我们如何抗衡双招,Lotalot淡笑一声:“很简单,我自己写不就是了”说完,他气息终于不再掩饰,显露而出,Go八股文小解!