面试题答案
一键面试Goroutine调度器原理
- M:N调度模型:Go语言采用M:N调度模型,即多个用户级线程(Goroutine)映射到多个操作系统线程(M)上。
- G - M - P模型
- G(Goroutine):代表一个轻量级的用户级线程,包含了执行的函数、栈以及一些状态信息。
- M(Machine):对应操作系统线程,负责执行Goroutine。每个M都有一个可运行Goroutine的本地队列。
- P(Processor):用于管理可运行的Goroutine队列,并为M提供执行环境。P的数量一般等于CPU核数,它持有一个全局Goroutine队列。
- 调度流程
- 创建Goroutine:当使用
go
关键字创建一个新的Goroutine时,它会被放入到P的本地队列或全局队列中。 - M获取Goroutine:M首先尝试从自己的本地队列获取Goroutine,如果本地队列为空,则尝试从P的本地队列窃取一半的Goroutine。若P的本地队列也为空,则从全局队列获取。
- 执行Goroutine:M从某个队列获取到Goroutine后,开始执行。当Goroutine执行系统调用(如I/O操作)时,M会将该Goroutine与自身分离,M去执行其他Goroutine,当系统调用完成,该Goroutine会被重新放入队列等待执行。
- 创建Goroutine:当使用
与传统操作系统线程调度机制对比
- 线程开销
- 传统线程:每个线程都需要占用较大的栈空间(通常几MB),创建和销毁的开销较大。
- Goroutine:栈空间初始时非常小(一般2KB),且可以按需动态增长和收缩,创建和销毁的开销极小。
- 调度方式
- 传统线程:由操作系统内核调度,上下文切换开销大,因为涉及到用户态到内核态的切换。
- Goroutine:由Go运行时调度器在用户态进行调度,上下文切换开销小,仅需保存和恢复少量寄存器的值。
- 资源占用
- 传统线程:线程数量受限于系统资源,过多线程会导致系统资源耗尽。
- Goroutine:由于其轻量级特性,可以创建大量Goroutine,轻松实现高并发。
Go调度器实现高效并发管理的方式
- 轻量级线程:Goroutine的轻量级特性使得可以创建大量并发任务,而不会消耗过多系统资源。
- 用户态调度:在用户态进行调度,减少了上下文切换开销,提高了调度效率。
- 工作窃取算法:M从P的本地队列窃取Goroutine,平衡了各个M之间的负载,充分利用CPU资源。
- 无锁数据结构:在调度器中使用了一些无锁数据结构,减少了锁竞争,提高并发性能。
高并发场景下的挑战及应对方法
- 锁竞争
- 挑战:高并发下,多个Goroutine同时访问共享资源时,容易产生锁竞争,降低性能。
- 应对:提倡使用通信来共享内存(如使用channel),减少对共享资源的直接访问;同时优化锁的使用,尽量缩短锁的持有时间。
- 栈溢出
- 挑战:虽然Goroutine栈可动态增长,但在极端情况下(如无限递归),仍可能导致栈溢出。
- 应对:Go运行时检测到栈溢出时,会自动分配新的栈空间,继续执行Goroutine。
- 调度延迟
- 挑战:大量Goroutine同时处于可运行状态时,调度器可能会出现调度延迟。
- 应对:通过工作窃取算法和合理调整P的数量,尽量减少调度延迟,确保每个Goroutine都能及时得到执行机会。