协程一直来说概念上挺玄学的,个人觉得确实是属于网上误传也非常多一种概念,经常跟用户级线程、绿色线程各种概念混在一起了。很多时候都说是一个东西,很多时候又能说出区别。留一个自己认知的大概解就差不多了,关键落到会用,跟线程的对比上有足够认知就差不多了。要提到这些概念也没问题,面试别咬死了说就好(我目前感觉这三个确实是一个东西)。● 不管定义是怎样,一定正确的一些特点: ○ 协作式,非抢占式,编程语言决定或者协程自己决定如何做切换调度 ○ 完全用户态,无内核开销,大多数操作不涉及系统调用● 跟线程对比 ○ 实现特点:线程是 os 的软中断实现的,内核决定主要调度,协程一般是自己让渡,很多时候是线程复用,无传统意义上的中断。 ○ 控制难度:这个是我之前没留意的一个点,感觉协程线程其实用起来都差不多。但协程确实提供了很多机制来降低控制难度,线程需要百分百管理锁和同步,而协程一般都会在语言运行时层面上进一步做抽象来简化并发模型。比如 go 里边的 wg 和 channel。 ○ 性能:更轻量实现并发。上下文切换、用户态、占用内存(栈空间)goroutine 是协程在 go 语言里的‘实现’(我觉得实现这种说法比是在 go 里面的‘优化’更好一点,因为协程本身就没有一个确定基础实现。所以其实就我目前的理解协程更像是满足上面特点的一种‘不同于多线程的程序并发实现思想’)● 基本认知 ○ 和线程 m 对 n 关系,可以并行 ○ 基于 GMP 模型,模型可以认知为整体的一个实现,其中 P 是作为调度器的角色,gm 分别代表 goroutine 和 内核线程的抽象。 ○ 几种调度方式:一种分类是主动、被动、正常、抢占四种情况。这个分类挺好的,列举了所有情况。主动是还没执行完交出执行权,被动是临时阻塞被调度器给调度了,正常调度是结束调度新的,抢占调度是后面版本引入的针对系统调用僵直情况的调度。前面三种都是协作式的符合协程特点。● 怎样做调度,大概的调度流程和机制,剩下有哪些调度细节 ○ gmp 模型中,p 主要是本地队列,m 主要是线程抽象,直接指向 g。执行设想就是,p 动态绑定一个 m,然后 m 再通过 p 去拿 g 去执行。 ○ m 怎么找到下一个正常执行的 g:主要按照本地队列、全局队列、wait 队列、work-stealing 的顺序取(再加上一个每 61 次会优先全局防止饥饿) ○ g 的生命周期,正常流程 idle -> dead -> runnable -> running -> dead,运行的两状态会分别进入一个系统调用状态和 wait 状态(即阻塞,就是上面的 wait 队列,由被动调度触发) ○ 我们直接创建的 g 优先进入当前 p 的本地队列,本地队列满或者主动让渡等情况会进全局队列● 其他面试问题 ○ 调度器 P 起到了什么作用?试想没有 P,那么我们 g 直接跟 m 绑定,那么好像其实就跟直接跟使用线程差不多了只是简单封装。首先最明显的作用就是实现了一个 m:n 关系,p 虽然跟处理器没直接关系但是还是确实代表一个抽象,最佳实践就是 p = 核心数(GOMAXPROCS),一定程度上线程的数量。其次这样动态绑定实现了线程的复用。除了这两个设计上的作用,剩下的就是他作为本地队列了,就类似于一个 channel 无锁化的重要步骤。最后还有 p 是作为一个所谓的“万能中间层”,除了动态做这种绑定复用,还能处理僵直的抢占调度的情况,这种情况下 p 会重新绑定一个 m 再去执行 g。 ■ 有个进阶题目,在一个 GOMAXPROCS 不等于核心数的 container 中运行程序会怎样?简单说,p 的数量直接意味着整个调度系统认为我们 cpu 有多少核,最大并行能力是怎样的。如果多了的话,就是实际线程数可能会比最佳值要大。那么就是调度效率下降,资源竞争加剧,性能下降。少了基本就是没充分利用了。 ○ 如何实现无锁化的。调度的时候取 g 的顺序,只有全局要加锁,p 是空的时候和窃取失败的时候都会提前取好 g。 ○ 为什么轻量?用户态,弱内核依赖,上下文切换,栈动态扩缩(初始内存是 0) ○ 操作系统如何向某个Goroutine发送时钟中断呢?(这个我暂时没找到答案,目前理解是 os 先通知 go runtime,然后相关 m 在自己执行过程中被插入系统调用。) ○ goroutine 终止会对其他 goroutine 产生影响吗(已捕获可恢复可以,否则会因为传递到顶层 g 导致全部崩溃) ○ 什么时候被回收(这个问题有点奇怪,了解所有调度方式就好了,正常用完都会被回收,不然就是 panic 恢复,程序结束)