Goroutine是Go里的一种轻量级线程——协程。相对线程,协程的优势就在于它非常轻量级,进行上下文切换的代价非常的小。对于一个goroutine ,每个结构体G中有一个sched的属性就是用来保存它上下文的。这样,goroutine 就可以很轻易的来回切换。由于其上下文切换在用户态下发生,根本不必进入内核态,所以速度很快。而且只有当前goroutine 的 PC, SP等少量信息需要保存。
在Go语言中,每一个并发的执行单元为一个goroutine。当我们开始运行一个Go程序时,它的入口函数 main 实际上就是运行在一个goroutine 里。
Goroutine之间的通信
Go 语言编写的程序通过不同的goroutine 运行,但是goroutine之间是相互独立的,各自运行在不同的上下文中。 每个 goroutine 之间的通信需要借助 channel ,channel 是Go 语言里的一种通信机制。Channel 也是Go语言里的一种引用类型,通过make函数,我们可以很容易的声明一个channel。
ch:=make(chanstring)
Channel 有单方向,双方向之分:
chan int, 双方向,可用来收发数据。
chan <- int,单方向,只能用来发送数据
<- chan int,单方向,只能用来接收数据
另外,channel还有有缓存无缓存之分。通过make函数创建一个带缓存的channel。
ch:=make(chanstring, n)
无缓存的channel保证了每次发送数据的同步接收操作。而带缓存的channel解耦了发送与接收间的操作,这样不但是影响程序的性能还有可能引起死锁的问题。
调度器sheduler
每个goroutine的运行都是由Go语言里的调度器(scheduler)决定的。
先说操作系统的线程调度。在POSIX 中有一个sheduler的内核函数,每过几ms会被执行一次。每次执行时,会挂起当前执行线程,同时保存它寄存器中信息,接着查看线程列表决定下一个线程的运行, 从内存中必复其寄存器信息和现场并开始执行。不同线程之间存在上下文切换,这包括保存一个用户线程的状态到内存,恢复另一个线程的信息到寄存器,同时还要更新sheduler相关的数据结构。这些操作都很耗时。
Go 语言的Runtime有自己的sheduler,通过它我们可以在n个操作系统的线程上调度m个goroutine。实际上Go 的sheduler与操作系统的sheduler是非常相似的,只不过它只关心goroutine的调度。与操作系统sheduler不同的是,Go的sheduler不使用硬件定时器,当一个goroutine 调用了time.Sleep、触发一个channel 操作或者使用 mutex, scheduler 会使这个 goroutine 进行睡眠,进而去唤醒另外一个goroutine,这种调度方式没有上下文之间的切换,它的代价比操作系统的线程调度要小得多。
Go的调度的实现,涉及到几个重要的数据结构。运行时库用这几个数据结构来实现goroutine的调度,管理goroutine和物理线程的运行。这些数据结构分别是结构体G,结构体M,结构体P,以及 Sched 结构体。这三个结构定义在文件 runtime/runtime.h 中,而Sched的定义在 runtime/proc.c 中。在Go语言中scheduler 通过一个GOMAXPROCS变量来决定有多少个操作系统的线程来运行Go程序,默认值为CPU的核心数。
结构体G
G 是 goroutine 的缩写,相当于操作系统中的进程控制块,在这里就是 goroutine 的控制结构,是对goroutine的抽象。其中包括 goid 是这个 goroutine 的ID, status 是这个goroutine 的状态,如 Gidle, Grunnable, Grunning, Gsyscall, Gwaiting,Gdead 等。
structG
{
uintptr stackguard; // 分段栈的可用空间下界
uintptr stackbase; // 分段栈的栈基址
Gobuf sched; //进程切换时,利用sched域来保存上下文
uintptr stack0;
FuncVal* fnstart; // goroutine运行的函数
void* param; // 用于传递参数,睡眠时其它goroutine设置param,唤醒时此goroutine可以获取
int16 status; // 状态 Gidle,Grunnable,Grunning,Gsyscall,Gwaiting,Gdead
int64 goid; // goroutine的id号
G* schedlink;
M* m; // for debuggers, but offset not hard-coded
M* lockedm; // G被锁定只能在这个m上运行
uintptr gopc; // 创建这个goroutine的go表达式的pc
...
};
结构体G中的部分域如上所示。可以看到,其中包含了栈信息stackbase和stackguard,有运行的函数信息fnstart。这些就足够成为一个可执行的单元了,只要得到CPU就可以运行。
goroutine 切换时,上下文信息保存在结构体的 sched 域中。goroutine 是轻量级的线程或者称为协程,切换时并不必陷入到操作系统内核中,所以保存过程很轻量。看一下结构体 G 中的 Gobuf,其实只保存了当前栈指针,程序计数器,以及 goroutine 自身。
structGobuf
{
// The offsets of these fields are known to (hard-coded in) libmach.
uintptr sp;
byte* pc;
G* g;
...
};
记录g是为了恢复当前goroutine的结构体G指针,运行时库中使用了一个常驻的寄存器extern register G* g,这个是当前goroutine的结构体G的指针。这样做是为了快速地访问goroutine中的信息,比如,Go的栈的实现并没有使用%ebp寄存器,不过这可以通过g->stackbase快速得到。”extern register”是由6c,8c等实现的一个特殊的存储。在ARM上它是实际的寄存器;其它平台是由段寄存器进行索引的线程本地存储的一个槽位。在linux系统中,对g和m使用的分别是0(GS)和4(GS)。需要注意的是,链接器还会根据特定操作系统改变编译器的输出,例如,6l/linux下会将0(GS)重写为-16(FS)。每个链接到Go程序的C文件都必须包含runtime.h头文件,这样C编译器知道避免使用专用的寄存器。
结构体M
M是machine的缩写,是对机器的抽象,每个m都是对应到一条操作系统的物理线程。M必须关联了P才可以执行Go代码,但是当它处理阻塞或者系统调用中时,可以不需要关联P。
structM
{
G* g0; // 带有调度栈的goroutine
G* gsignal; // signal-handling G 处理信号的goroutine
void (*mstartfn)(void);
G* curg; // M中当前运行的goroutine
P* p; // 关联P以执行Go代码 (如果没有执行Go代码则P为nil)
P* nextp;
int32 id;
int32 mallocing; //状态
int32 throwing;
int32 gcing;
int32 locks;
int32 helpgc; //不为0表示此m在做帮忙gc。helpgc等于n只是一个编号
bool blockingsyscall;
bool spinning;
Note park;
M* alllink; // 这个域用于链接allm
M* schedlink;
MCache *mcache;
G* lockedg;
M* nextwaitm; // next M waiting for lock
GCStats gcstats;
...
};
和G类似,M中也有alllink域将所有的M放在allm链表中。lockedg是某些情况下,G锁定在这个M中运行而不会切换到其它M中去。M中还有一个MCache,是当前M的内存的缓存。M也和G一样有一个常驻寄存器变量,代表当前的M。同时存在多个M,表示同时存在多个物理线程。结构体M中有两个G是需要关注一下的,一个是curg,代表结构体M当前绑定的结构体G。另一个是g0,是带有调度栈的goroutine,这是一个比较特殊的goroutine。普通的goroutine的栈是在堆上分配的可增长的栈,而g0的栈是M对应的线程的栈。所有调度相关的代码,会先切换到该goroutine的栈中再执行。
结构体P
Go1.1中新加入的一个数据结构,它是Processor的缩写。结构体P的加入是为了提高Go程序的并发度,实现更好的调度。M代表OS线程。P代表Go代码执行时需要的资源。当M执行Go代码时,它需要关联一个P,当M为idle或者在系统调用中时,它也需要P。有刚好GOMAXPROCS个P。所有的P被组织为一个数组,在P上实现了工作流窃取的调度器。
structP
{
Lock;
uint32 status; // Pidle或Prunning等
P* link;
uint32 schedtick; // 每次调度时将它加一
M* m; // 链接到它关联的M (nil if idle)
MCache* mcache;
G* runq[256];
int32 runqhead;
int32 runqtail;
// Available G's (status == Gdead)
G* gfree;
int32 gfreecnt;
byte pad[64];
};
结构体P中也有相应的状态:Pidle, Prunning, Psyscall, Pgcstop, Pdead。跟G不同的是,P 不存在waiting状态。跟G不同的是,P不存在waiting状态。MCache被移到了P中,但是在结构体M中也还保留着。在P中有一个Grunnable的goroutine队列,这是一个P的局部队列。当P执行Go代码时,它会优先从自己的这个局部队列中取,这时可以不用加锁,提高了并发度。如果发现这个队列空了,则去其它P的队列中拿一半过来,这样实现工作流窃取的调度。这种情况下是需要给调用器加锁的。
结构体Sched
Sched是调度实现中使用的数据结构,该结构体的定义在文件proc.c中。
structSched {
Lock;
uint64 goidgen;
M* midle; // idle m's waiting for work
int32 nmidle; // number of idle m's waiting for work
int32 nmidlelocked; // number of locked m's waiting for work
int3 mcount; // number of m's that have been created
int32 maxmcount; // maximum number of m's allowed (or die)
P* pidle; // idle P's
uint32 npidle; //idle P的数量
uint32 nmspinning;
// Global runnable queue.
G* runqhead;
G* runqtail;
int32 runqsize;
// Global cache of dead G's.
Lock gflock;
G* gfree;
int32 stopwait;
Note stopnote;
uint32 sysmonwait;
Note sysmonnote;
uint64 lastpoll;
int32 profilehz; // cpu profiling rate
}
大多数需要的信息都已放在了结构体M、G和P中,Sched结构体只是一个壳。可以看到,其中有M的idle队列,P的idle队列,以及一个全局的就绪的G队列。Sched结构体中的Lock是非常必须的,如果M或P等做一些非局部的操作,它们一般需要先锁住调度器。
Goroutine的特点
Goroutine是Go Runtime所提供的,并非操作系统层面上支持的,goroutine不是用线程实现的。goroutine就是一段代码,一个函数入口,以及在堆上为其分配的一个堆栈。这个栈通常很小,一般为2kB, 所以它非常廉价,我们可以很轻松的创建上成千万个goroutine,这是很普遍的。Goroutine 的栈大小不是固定的,这一点和操作系统的线程是不一样的,它可以动态的扩展,最大值可达1GB。
Goroutine是协作式调度的,如果goroutine会执行很长时间,而且不是通过等待读取或写入channel的数据来同步的话,就需要主动调用Gosched()来让出CPU。
Go语言封装了异步IO,所以可以写出貌似并发数很多的服务端,可即使我们通过调整GOMAXPROCS来充分利用多核CPU并行处理,其效率也不如我们利用IO事件驱动设计的、按照事务类型划分好合适比例的线程池。在响应时间上,协作式调度是硬伤。
每个goroutine是没有身份标识的,这是为了避免像Thread Local Storage那样被烂用,一个函数的行为不可能由其本身变量决定。
Goroutine最大的优点是在并发开发中实现了对线程池的动态扩展,不会由于某个任务的阻塞而导致死锁。随着其运行库的不断发展和完善及多核大行其道的年代,其优势会日益凸显。
下面来看一个实例。
实例:生产者消费者问题
通过goroutine实现生产者消费者问题,利用 channel 通信。只需要短短几行代码,我们自己根本不需要编写代码考虑线程的同步问题。
需要事先声明的变量,goods 是生产消费所共享的数据,声明为一个chan 类型,存放整型数据。接着声明一个随机数种子,根据系统时间生成伪随机数。done 也是一个 chan 类型,里面只存储一个空的struct,其作用是为了保证主线程在其它 goroutine 结束之后结束。
var goods chan int
var r = rand.New(rand.NewSource(time.Now().UnixNano()))//定义一个随机数种子
vardone chan struct{}
生产者函数,循环10次,依次向 goods里写入1到10,每写完一次后,随机睡眠1~3秒。
funcproduce() {
for i:=1; i<=10; i++ {
goods <- i
time.Sleep(time.Duration(r.Int31n(3))*time.Second)
}
done <-struct{}{}
}
消费者函数,循环5次从 goods 里取值,每读完一次,随机睡眠1~5秒。
funcconsume() {
for i:=0; i<5; i++ {
good := <- goods
fmt.Printf("The goods size is : %v\n",10-good+1)
time.Sleep(time.Duration(r.Int31n(5))*time.Second)
}
}
main 函数里开启一个goroutine 运行 produce 函数,两个goroutine 运行consume 函数。
funcmain() {
goods =make(chanint)
done =make(chanstruct{})
goproduce()
goconsume()
goconsume()
<- done
}
output:
The goods size is :10
The goods size is :9
The goods size is :8
The goods size is :7
The goods size is :6
The goods size is :5
The goods size is :4
The goods size is :3
The goods size is :2
The goods size is :1
参考资料:
[深入解析Go](https://tiancaiamao.gitbooks.io/go-
internals/content/zh/05.1.html)