线程
通常语义中的线程,指的是内核级线程,核心点如下:
(1)是操作系统最小调度单元;
(2)创建、销毁、调度交由内核完成,cpu 需完成用户态与内核态间的切换;
(3)可充分利用多核,实现并行.
普通协程
又称为用户级线程,核心点如下:
(1)与线程存在映射关系,为 M:1;
(2)创建、销毁、调度在用户态完成,对内核透明,所以更轻;
(3)从属同一个内核级线程,无法并行;一个协程阻塞会导致从属同一线程的所有协程无法执行.
因为普通协程附着于某一个线程,在协程阻塞状态下,影响会上升到线程级别导致此线程阻塞并且附着于此线程的所有协程都将进入阻塞状态。所以理论上协程并不能做到真正意义上的并行,只能并发执行。在内核态视角下只能看到此线程出现了异常情况并无法感知到是某一协程的问题。
Goroutine
Goroutine,经 Golang 优化后的特殊“协程”(没有什么东西是在中间加一层不能解决的_go语言的调度器processor),核心点如下:
-
与线程存在映射关系,为 M:N 多对多;
-
创建、销毁、调度在用户态完成无需内核态参与,对内核透明,足够轻便;
-
可利用多个线程,实现并行;
-
通过调度器的斡旋,实现和线程间的动态绑定和灵活调度;举个例子:当某一个Goroutine陷入了阻塞 go调度器会感知到此异常并解绑此Goroutine与线程的绑定,将资源释放出来再执行调度器的Goroutine
-
栈空间大小可动态扩缩,因地制宜.不会存在空间上的浪费.普通协程默认4M会造成不必要的浪费
对比
综上,goroutine 可说是博采众长之物.
实际上,“灵活调度” 一词概括得实在过于简要,Golang 在调度 goroutine 时,针对“如何减少加锁行为”,“如何避免资源不均”等问题都给出了精彩的解决方案,这一切都得益于经典的 “gmp” 模型
GMP模型
综上,goroutine 可说是博采众长之物.
实际上,“灵活调度” 一词概括得实在过于简要,Golang 在调度 goroutine 时,针对“如何减少加锁行为”,“如何避免资源不均”等问题都给出了精彩的解决方案,这一切都得益于经典的 “gmp” 模型,而这些,就留待第 2 节展开介绍.
G是什么?
-
g 即goroutine,是 golang 中对协程的抽象;
-
g 有自己的运行栈、状态、以及执行的任务函数(用户通过 go func 指定);
-
g 需要绑定到 p 才能执行,在 g 的视角中,p 就是它的 cpu.
P是什么?
-
p 即 processor,是 golang 中的调度器;使G和M之间的动态映射
-
p 是 gmp 的中枢,借由 p 承上启下,实现 g 和 m 之间的动态有机结合;
-
对 g 而言,p 是其 cpu,g 只有被 p 调度,才得以执行;
-
对 m 而言,p 是其执行代理,为其提供必要信息的同时(可执行的 g、内存分配情况等),并隐藏了繁杂的调度细节;
-
p 的数量决定了 g 最大并行数量,可由用户通过 GOMAXPROCS 进行设定(超过 CPU 核数时无意义).
M是什么?
- m 即 machine,是 golang 中对线程的抽象;可以理解为内核视角下的线程
- m 不直接执行 g,而是先和 p 绑定,由其实现代理;
- 借由 p 的存在,m 无需和 g 绑死,也无需记录 g 的状态信息,因此 g 在全生命周期中可以实现跨 m 执行.