https://www.cnblogs.com/qcrao-2018/p/11442998.html
为什么要使用 Go 语言?Go 语言的优势在哪里?
goroutine协程让出、抢占、监控、调度
多个线程可以属于同一个进程并共享内存空间。因为多线程不需要创建新的虚拟内存空间,所以它们也不需要内存管理单元处理上下文的切换,线程之间的通信也正是基于共享的内存进行的,与重量级的进程相比,线程显得比较轻量。
虽然线程比较轻量,但是在调度时也有比较大的额外开销。每个线程会都占用 1M 以上的内存空间,在切换线程时不止会消耗较多的内存,恢复寄存器中的内容还需要向操作系统申请或者销毁资源,每一次线程上下文的切换都需要消耗 ~1us 左右的时间1,但是 Go 调度器对 Goroutine 的上下文切换约为 ~0.2us,减少了 80% 的额外开销2。
Go 语言的调度器通过使用与 CPU 数量相等的线程减少线程频繁切换的内存开销,同时在每一个线程上执行额外开销更低的 Goroutine 来降低操作系统和硬件的负载。
Goroutine 是一个与其他 goroutines 并行运行在同一地址空间的 Go 函数或方法。一个运行的程序由一个或更多个
goroutine 组成。它与线程、协程、进程等不同。它是一个 goroutine” —— Rob Pike
Goroutines 在同一个用户地址空间里并行独立执行 functions,channels 则用于 goroutines 间的通信和同步访问控制。
创建一个 goroutine 的栈内存消耗为 2 KB(Linux AMD64 Go v1.4后),运行过程中,如果栈空间不够用,会自动进行扩容。
创建一个 thread 为了尽量避免极端情况下操作系统线程栈的溢出,默认会为其分配一个较大的栈内存( 1 - 8 MB 栈内存,线程标准 POSIX Thread),而且还需要一个被称为 “guard page” 的区域用于和其他 thread 的栈空间进行隔离。而栈内存空间一旦创建和初始化完成之后其大小就不能再有变化,这决定了在某些特殊场景下系统线程栈还是有溢出的风险。
线程创建和销毀都会有巨大的消耗,是内核级的交互(trap)。
POSIX 线程(定义了创建和操纵线程的一套 API)通常是在已有的进程模型中增加的逻辑扩展,所以线程控制和进程控制很相似。而进入内核调度所消耗的性能代价比较高,开销较大。goroutine 是用户态线程,是由 go runtime 管理,创建和销毁的消耗非常小。
抛开陷入内核,线程切换会消耗 1000-1500 纳秒(上下文保存成本高,较多寄存器,公平性,复杂时间计算统计),一个纳秒平均可以执行 12-18 条指令。
所以由于线程切换,执行指令的条数会减少 12000-18000。goroutine 的切换约为 200 ns(用户态、3个寄存器),相当于 2400-3600 条指令。因此,goroutines 切换成本比 threads 要小得多。
另外,goroutines是协作式调度
线程的创建和退出复杂,多个 thread 间通讯复杂(share memory)。
不能大量创建线程(参考早期的 httpd),成本高,使用网络多路复用,存在大量callback(参考twemproxy、nginx 的代码)。对于应用服务线程门槛高,例如需要做第三方库隔离,需要考虑引入线程池等。
Go 创建 M 个线程(CPU 执行调度的单元,内核的 task_struct),之后创建的 N 个 goroutine 都会依附在这 M 个线程上执行,即 M:N 模型。它们能够同时运行,与线程类似,但相比之下非常轻量。因此,程序运行时,Goroutines 的个数应该是远大于线程的个数的(phread 是内核线程?)。
同一个时刻,一个线程只能跑一个 goroutine。当 goroutine 发生阻塞 (chan 阻塞、mutex、syscall 等等) 时,Go 会把当前的 goroutine 调度走,让其他 goroutine 来继续执行,而不是让线程阻塞休眠,尽可能多的分发任务出去,让 CPU 忙。
Go 创建 M 个线程(CPU 执行调度的单元,内核的 task_struct),之后创建的 N 个 goroutine 都会依附在这 M 个线程上执行,即 M:N 模型。它们能够同时运行,与线程类似,但相比之下非常轻量。因此,程序运行时,Goroutines 的个数应该是远大于线程的个数的(phread 是内核线程?)。
同一个时刻,一个线程只能跑一个 goroutine。当 goroutine 发生阻塞 (chan 阻塞、mutex、syscall 等等) 时,Go 会把当前的 goroutine 调度走,让其他 goroutine 来继续执行,而不是让线程阻塞休眠,尽可能多的分发任务出去,让 CPU 忙。
start
启动 m0 主线程,初始化 g0 负责 schedule,初始化 P, sysmon 线程, GC 协程;
然后 P 负责创建 os thread 关联 M;
G 切换时,暂存当前的 PC 及 GO 堆栈即可;
回收:G 用完会放到空闲列表,由 P 来负责回收
runtime.gopark 是触发调度最常见的方法,该函数会将当前 Goroutine 暂停,被暂停的任务不会放回运行队列,我们来分析该函数的实现原理:
函数 诱发的因素 主要场景为:
我们可以根据这些特性,去拆解可能会造成阻塞的原因。其实也就没必要记了,他们会导致阻塞肯定是由于存在影响控制流的因素,才会导致 gopark 的调用。
runtime.park_m 会将当前 Goroutine 的状态从 _Grunning 切换至 _Gwaiting,调用 runtime.dropg 移除线程和 Goroutine 之间的关联,在这之后就可以调用 runtime.schedule 触发新一轮的调度了。
当 Goroutine 等待的特定条件满足后,运行时会调用 runtime.goready 将因为调用 run