Goroutine Base
Goroutine
Goroutine是什么
goroutine是一种轻量级线程,也称之为协程。其创建与销毁不需要经过内核而是直接在go运行时进行管理。相比线程来说其更轻量,不仅仅在创建销毁过程中速度快,而且还在上下文切换以及内存分配都优于线程。
Go语言中创建goroutine
在Go语言创建goroutine很简单不需要导入其它库,go语言本身就自带了go关键字来创建goroutine。
func TestGoroutine(t *testing.T) {
createGoroutine()
}
func createGoroutine() {
go PrintHelloWorld() #创建goroutine
}
func PrintHelloWorld() {
fmt.Println("Hello, World!")
}
go关键字用于在新的goroutine中启动指定的函数。现有的goroutine会与新创建的goroutine同时运行。作为goroutine运行的函数可以接收参数,但不能返回值。goroutine函数的参数在goroutine启动前就会被求值,并且在goroutine开始运行后传递给函数。
创建goroutine所需的资源
栈空间
栈初始大小:Go1.19版本及以后的版本,运行时使用历史平均值,早期版本1.4+为2K。
**分配方式:**初始栈式连续的内存块,由Go运行时的内存分配器(不是系统调用)在用户态分配。
**动态特性:**栈空间可以自动扩容最大可达1GB,不像OS 线程那样是固定栈大小。扩容时会分配新的更大栈块,把旧数据拷贝过去,全程在用户态完成,无需内核介入。
Goroutine控制块(G结构体)
g结构体是描述goroutine状态核心的数据结构,创建时会分配以下资源:
- stack: 栈的起始地址、结束地址、当前栈指针。
- status: 运行状态(Gidle就绪、运行Grunning、阻塞Gwating、退出Gdead等)
- m: 绑定的M(Machine,对应OS线程)指针
- p:绑定的P(Processor 逻辑处理器)指针
- sched: 调度上下文(寄存器值、程序计数器PC等,用于Gouroutine切换时的保存状态)
- goid: Goroutine唯一ID用于标识不同的
Goroutine。 - waitreason: 阻塞原因(如等待chanel、锁、IO等)
内存占用: g结构体本身大小几百字节,远小于OS线程的TCP(线程控制块)。
调度相关的元数据
- 就绪队列节点: 当创建一个goroutine后,比如分配完栈空间和g结构体之后,会将该goroutine加入到P的本地就绪队列(或者全局队列),仅占用队列的一个节点位置,指针大小。
- GC相关标记: Go运行时会为
goroutine标记GC根对象(栈中的指针),无需额外分配内存,仅做状态标记。
main函数所在goroutine终止了
在运行我们的程序时,go运行时会创建多个goroutine。具体多少根据不同版本的实现方式不同而不同。但至少会有一个用于垃圾回收的goroutine和一个主goroutine,也就是执行main函数的goroutine。当main函数执行完成之后,程序会立即终止,所有正在运行的goroutine都会在函数执行中途突然终止,无法执行任何清理操作。
func TestGoroutine(t *testing.T) {
mainFunc()
}
func mainFunc() {
fmt.Println("main函数开始执行", goid.Get())
//创建goroutine
go longTimeRun()
fmt.Println("main函数退出")
}
func longTimeRun() {
fmt.Println("我正在执行", goid.Get())
time.Sleep(time.Second) //休眠一秒
fmt.Println("我准备退出了", goid.Get())
}
// output:
=== RUN TestGoroutine
main函数开始执行 19
main函数退出
--- PASS: TestGoroutine (0.00s)
PASS
我正在执行 20
ok go-concurrency-base/goroutine 1.916s
Goroutine发生了panic会导致主程序退出吗
panic可以终止goroutine,如果goroutine中发生panic,它会沿着调用栈向上传播,知道找到recover函数或者gorotine返回。如果panic未被处理,将会输出panic消息,程序随之崩溃。
func TestGoroutine(t *testing.T) {
go riskTask()
time.Sleep(100)
fmt.Println("是否正确执行")
}
func riskTask() {
var p *int
fmt.Println(*p) // 解引用空指针
}
//程序会直接引发panic 导致整个程序崩溃
如果在riskTask中捕获panic然后recover,会发现程序不会因为一个goroutinepanic了导致整个程序都崩溃。
func TestGoroutine(t *testing.T) {
go riskTaskWithRecover()
time.Sleep(100)
fmt.Println("是否正确执行")
}
func riskTaskWithRecover() {
var p *int
defer func() {
if err := recover(); err != nil {
debug.PrintStack()
}
}()
fmt.Println(*p)
}
重点:解决goroutine引发panic导致程序崩溃需要使用defer+recover来捕获panic然后处理,但是recover只能捕获当前goroutine不能跨goroutine。所以如果一个goroutine并没有处理panic那么整个程序都崩溃了。