面试题答案
一键面试Goroutine启动过程
- 创建G结构体:Go运行时系统为每个Goroutine创建一个
G
结构体,该结构体包含了Goroutine的栈空间、程序计数器(PC)、Goroutine状态等信息。栈空间在创建时一般有一个较小的初始大小,会随着需求动态增长和收缩。 - 调度器管理:创建好的
G
结构体被放入调度器的全局或者局部队列中。Go语言的调度器采用M:N调度模型(多个Goroutine映射到多个操作系统线程),调度器的主要职责是管理G
(Goroutine)、M
(操作系统线程)和P
(处理器,用于执行Goroutine的上下文)。 - 执行:当一个
M
(操作系统线程)获取到一个P
(处理器)时,它会从P
关联的本地队列或者全局队列中取出一个G
来执行。执行过程中,Goroutine会按照其代码逻辑运行,遇到I/O操作、系统调用或者主动调用runtime.Gosched()
等情况时,会让出P
,使得调度器可以安排其他Goroutine执行。
传统线程启动过程
- 系统调用:在大多数操作系统中,通过系统调用(如
pthread_create
在POSIX系统中)来创建线程。这个系统调用会通知操作系统内核,内核为新线程分配内核资源,包括独立的栈空间、线程控制块(TCB,包含线程状态、寄存器值等信息)等。 - 线程初始化:内核初始化线程的上下文,设置栈指针、程序计数器等。初始化完成后,线程进入可运行状态,被放入操作系统的线程调度队列中。
- 调度执行:操作系统的调度器根据其调度算法(如时间片轮转、优先级调度等),从调度队列中选择线程并分配CPU时间片,线程开始执行其指定的函数。
资源消耗差异
- 栈空间:
- Goroutine:初始栈空间通常较小(如2KB),且栈空间可动态增长和收缩,在不需要大量栈空间时,占用资源少。
- 传统线程:栈空间一般在创建时就固定分配,通常较大(如几MB),即使实际使用的栈空间远小于分配的大小,也会占用该固定大小的内存,导致内存浪费。
- 内核资源:
- Goroutine:运行在用户态,不需要内核为每个Goroutine分配专门的内核资源,多个Goroutine复用少量的操作系统线程(
M
),因此整体内核资源消耗少。 - 传统线程:每个线程都需要内核分配独立的内核资源(如TCB等),随着线程数量的增加,内核资源消耗显著增加。
- Goroutine:运行在用户态,不需要内核为每个Goroutine分配专门的内核资源,多个Goroutine复用少量的操作系统线程(
启动速度差异
- Goroutine:创建过程只是在用户态进行一些简单的内存分配和数据结构初始化(创建
G
结构体等),不涉及系统调用,所以启动速度非常快,可以在短时间内创建大量的Goroutine。 - 传统线程:创建需要通过系统调用进入内核态,内核进行资源分配和初始化等操作,系统调用开销较大,导致线程启动速度相对较慢,创建大量线程时性能开销明显。