goroutine是Go实现的用户态线程,主要用来解决操作系统线程太【重】的问题。
操作系统线程的重,主要体现在以下两方面:
-
创建和切换。操作系统线程的创建和切换操作都需要进入内核,而进入操作系统内核性能代价较高,开销较大。
-
内存使用。其一是内核为了避免极端情况下操作系统线程栈溢出,所以在创建线程时,会为其分配一个较大的栈内存(虚拟地址空间,内核并不会一开始就分配这么多物理内存),大多数情况下操作系统线程远远用不了这么多内存,这就导致了内存浪费,另外一点就是栈内存空间一旦创建和初始化完成后,其大小就不能再产生变化,这导致了在某特殊环境下线程栈还是有溢出的风险。
用户态的goroutine的【轻】则体现在如下两个方面:
-
goroutine是用户态线程,创建和切换都在用户状代码中完成,而无需进入系统内核,所以其开销要远小于系统线程的创建和切换。
-
goroutine启动时栈大小默认为2K,大多数情况下是够用的,如果不够的话,goroutine的栈会自动扩大,同时,如果栈内存太大过于浪费,还能自动收缩,如此既没有栈溢出的风险,也不会造成栈内存空间过大的浪费。
正是因为如上原因,使得Go程序中可轻易创建很多goroutine来并发执行任务,而不用过多考虑性能和内存的问题。
来看下goroutine的线程模型,之后简单介绍下goroutine调度器的。
goroutine建立在系统线程之上,与系统线程实现了多对多(M:N)的两级线程模型。
这时的M:N指的是M个goroutine运行在N个系统线程之上,内核负责对这N个系统线程进行调度,而这N个系统线程又负责对这M个goroutine进行调度和运行。
系统线程对goroutine的调度指的是程序代码按照一定算法在一定时间挑选出合适的goroutine并放到CPU上运行的过程。
这些负责负责对goroutine进行调度的程序代码称之为goroutine调度器。
来看下goroutine调度器工作的伪代码:
// 程序启动时的初始化代码
......
for i := 0; i < N; i++ { // 创建N个操作系统线程执行schedule函数
create_os_thread(schedule) // 创建一个操作系统线程执行schedule函数
}
//schedule函数实现调度逻辑
func schedule() {
for { //调度循环
// 根据某种算法从M个goroutine中找出一个需要运行的goroutine
g := find_a_runnable_goroutine_from_M_goroutines()
run_g(g) // CPU运行该goroutine,直到需要调度其它goroutine才返回
save_status_of_g(g) // 保存goroutine的状态,主要是寄存器的值
}
}
上述代码意为程序运行起来之后创建了N个由内核调度的系统线程,也可以称为工作线程,来执行schedule,而schedule在一个调度循环中反复从M个goroutine中挑选出一个需要执行的goroutine,并跳转到goroutine去执行,直到需要调度其它goroutine时才返回到schedule中通过save_status_of_g保存刚刚正在运行的goroutine的状态,然后再去寻找下一个goroutine。
上述代码为高度抽象且做了修改和简化,只是为了帮助宏观理解goroutine两级调度模型,具体的实现原理和细节在下篇文章中会慢慢聊。
以上仅为个人观点,不一定准确,能帮到各位那是最好的。
好啦,到这里本文就结束了,喜欢的话就来个三连击吧。
扫码关注公众号,获取更多优质内容。