Golang的goroutine介绍
当谈到Golang(又名Go)的特色时,一个特别突出的话题就是它的goroutine。Goroutine是一种轻量级的线程,Golang的并发模型是基于它们构建的。在本篇文章中,我们将会深入探究Golang的goroutine是如何实现的。
Goroutine的简介
Goroutine是一种比传统线程更加轻量级的并发处理方式。每一个Goroutine都是由Golang的运行时(runtime)所管理的,它们在同一个进程中运行,但是它们却可以同时执行不同的任务。因为Goroutine的轻量级,一个程序可以启动成百上千个Goroutine来执行任务,而不会对程序的性能产生太大的影响。
Goroutine的实现原理
在Golang中,goroutine的实现是通过运行时(runtime)来完成的。当我们创建一个goroutine时,runtime会帮我们在操作系统线程中启动一个goroutine并分配一个栈(Stack)来保存它的执行状态。
当一个goroutine被创建时,它会被加入到Golang的调度器(Scheduler)中,这个调度器是由runtime来管理的。调度器会决定哪个goroutine可以运行,哪个goroutine需要暂停等等。
Golang的调度器采用的是M:N的调度模型。也就是说,它将M个goroutine(M是操作系统线程的数量)映射到N个操作系统线程上。当一个goroutine需要执行时,调度器会从空闲的线程池中选取一个线程来执行这个goroutine。
调度器会监视每个goroutine的执行状态,如果一个goroutine阻塞了(比如等待I/O操作的完成),那么调度器会将这个goroutine暂停并执行另一个可用的goroutine。当被阻塞的goroutine再次可用时,调度器会再次将它放回运行队列中。
Goroutine的底层数据结构
在 Golang 中,每个 goroutine 是由 G 对象表示的,它包含了 goroutine 的栈、goroutine 的状态以及相关的信息。在 Golang 的运行时中,还有其他的结构体和数据结构来支持 goroutine 的实现和调度。
下面是 goroutine 的底层数据结构:
G (goroutine) 结构体:表示一个 goroutine。它包含了 goroutine 的栈、指令指针、程序计数器、状态、等待队列等信息。其中,状态字段包括以下几种状态: _Gidle:空闲状态,等待分配任务。 _Grunnable:可运行状态,等待被调度执行。 _Grunning:正在执行状态,即当前正在运行的 goroutine。 _Gsyscall:系统调用状态,等待系统调用返回。 _Gwaiting:等待状态,等待某个事件(比如 channel 的读写、锁的释放等)。 _Gdead:死亡状态,goroutine 已经完成任务或被取消。
通过示例展示部分核心源码
以下是一个简单的 Go 程序,其中包含了创建和运行 goroutine 的代码。我们可以结合该程序的源码来展示 goroutine 的实现。
package main import ( "fmt" "time" ) func main() { go sayHello() // 创建一个新的 goroutine 来执行 sayHello 函数 time.Sleep(1 * time.Second) fmt.Println("Main goroutine exit.") } func sayHello() { for i := 0; i < 5; i++ { fmt.Println("Hello", i) time.Sleep(100 * time.Millisecond) } }
在上述代码中,我们通过 go 关键字创建了一个新的 goroutine 来执行 sayHello 函数。这个 goroutine 会和主 goroutine 并发运行,互不干扰。
现在让我们来看一下 go 关键字的源码实现。在 Go 源码中,go 关键字的实现实际上是一个函数调用,该函数的定义如下:
func goFunc(fn *funcval, argp unsafe.Pointer, n int, callerpc uintptr)
该函数接受四个参数:
fn:函数指针,表示需要执行的函数。 argp:参数指针,表示函数的参数。 n:参数个数,表示函数的参数个数。 callerpc:调用者 PC 寄存器的值,用于调试和跟踪。
在函数内部,Go runtime 会创建一个新的 G 对象,并将该 G 对象的状态设置为 Grunnable,然后将其插入到某个 P 的本地队列或全局队列中等待被调度。
下面是 go 关键字的简化版源码实现:
func goFunc(fn *funcval, argp unsafe.Pointer, n int, callerpc uintptr) { newG := getg() // 获取当前 goroutine 的 G 对象 if newG == oldG { newG = newproc() // 创建一个新的 P,并启动一个新的 M 来执行该 P } newG.sched.pc = callerpc // 设置新 goroutine 的调用者 PC 寄存器 newG.sched.sp = uintptr(argp) // 设置新 goroutine 的堆栈指针 newG.sched.argp = argp // 设置新 goroutine 的参数指针 newG.sched.arglen = uintptr(n) // 设置新 goroutine 的参数个数 newG.sched.fn = fn // 设置新 goroutine 需要执行的函数 if atomic.Cas(&sched.runqsize, sched.runqsize, sched.runqsize+1) { lock(&sched.lock) // 获取全局调度器锁 if sched.runqtail == nil { sched.runqhead = newG sched.runqtail = newG } else { sched.runqtail.sched.link = newG sched.runqtail = newG } unlock(&sched.lock) // 释放全局调度器锁 } }
Goroutine的优点
相较于传统线程的实现方式,Goroutine具有以下几个优点:
Goroutine是轻量级的。由于它们是由Golang的运行时管理的,所以它们的创建和销毁的代价非常小,可以轻松地创建大量的goroutine来完成任务。
Goroutine的切换代价低。由于Golang的调度器是基于协作式的调度模型,所以当一个goroutine被阻塞时,调度器可以立即切换到另一个可用的goroutine,而不需要等待操作系统的线程切换。
Goroutine具有内置的同步机制。Golang提供了一系列内置的同步机制(比如channels),可以用来协调不同的goroutine之间的通信和同
小结
总的来说,Golang 的 goroutine 是基于协作式的调度模型实现的,它通过调度器来分配不同的 goroutine 执行任务,从而实现并发。同时,Golang 还提供了丰富的内置函数和工具来协调不同 goroutine 之间的通信和同步。