进程&&线程&&协程
"三程":
- 进程:程序的一次运行,资源分配(除CPU)的基本单位;
- 线程:CPU调度的基本单位;
- 协程:一种用户态、轻量级的线程;
用户态线程:
- 内核看不到,不知道用户线程的存在,内核调度时,仍然以该进程为单位进行调度;
- 用户级线程内核的切换由用户态程序自己控制内核切换(通过系统调用来获得内核提供的服务),不需要内核干涉,少了进出内核态的消耗,但不能很好的利用多核Cpu。
- 任意时刻,每个进程只能够有一个用户线程在运行,尽管它是多线程的,这就不利于多核CPU的使用了;
协程就是一种用户态线程;
优点:线程的切换无需陷入内核,所以切换开销小,速度非常快;
缺点:同一个进程中同时只有一个线程在运行,一个线程的阻塞将导致整个进程的阻塞;
核心态线程:
- 由内核进行管理,线程切换将会消耗较多资源。对于多核CPU,可进行多线程并行运行;
- 内核能够感知到线程的存在,CPU调度时,是以线程为单位进行调度;
优点:可以多线程并行运行;
缺点:线程切换时,比较消耗资源。因为需要保存上下文,核心态和用户态的切换;
两个切换的对比:
- 用户线程切换:只设计基本的CPU上下文切换。切换是完全在用户态中进行的;
- 内核线程的切换:也有上下文切换,且保存的东西更多一点。并且线程的调度只有内核才能完成,这就涉及到了用户态和核心态的切换,这才是最主要的开销;(进程的切换也要陷入内核)
内核线程和用户线程的联系
- 一对一模型:充分利用了多核系统的优势但是上下文切换非常慢,因为每一次调度都会在用户态和内核态之间切换。POSIX线程模型(pthread)就是这么做的。
- 多对一模型:多个用户空间线程在1个内核空间线程上运行。 优势是上下文切换非常快,因为只有一个内核线程嘛,用户线程切换时,内核的这个线程是不用动的。也就意味着没有了核心态和用户态的切换,以及内核线程的上下文保存。
- 多对多模型:效率虽高,但是管理复杂;
Golang的Goroutine调度模型
goroutine就是协程,完全运行在用户态中,借鉴了N:M模型;
1、GMP模型
- G:
goroutine
- M:
Machine
,内核线程 - P:
Logical Processor
,处理器;代表了M所需要的上下文环境runtime.GOMAXPROCS (numLogicalProcessors)
可以设置多少个处理器,go 1.5开始,默认是CPU核数;- 实际运行时P和CPU核心数并无任何关联,P最大不超过256;P可以理解为并行度的多少,也就是说当前最多只能有P个线程在运行;(是不是很像线程池)
- P一旦初始化了,就不能修改了;
三者关系:
- M的数量和P不一定匹配,可以设置很多M,M和P绑定后才可运行,多余的M处于休眠状态。
- P包含一个
LRQ(Local Run Queue)
本地运行队列,这里面保存着P需要执行的协程G的队列。 - 除了每个P自身保存的G的队列外,调度器还拥有一个全局的G队列
GRQ(Global Run Queue)
,这个队列存储的是所有未分配的协程G。
2、go func()执行流程
- 创建一个G对象,加入到本地队列或全局队列;
- 如果还有空闲的P,则创建一个M;
- M会启动一个底层线程,并结合P,循环执行G;
- P执行G的顺序是,先从本地队列找,没有则到全局队列找(一次性转移[
全局G个数/P个数
]),再到其他P中找(一次性转移一半
); - G是执行顺序是按照队列顺序的;
- P管理着G队列,但是G要运行,还需要M的绑定;
runtime.GOMAXPROCS
只会影响P的数量,不会影响M的数量;- P和M的关系,就好比用户线程和内核线程的
N:M模型
; - 没有足够的M关联P时,会创建M;在
runtime
执行系统监控或垃圾回收等任务的时候也会导致新的M的创建。 所以,runtime.GOMAXPROCS
只是类似线程池的大小设置而已; - 当然,go也可以通过
runtime/debug.SeMaxThreads
限制操作系统线程数;SetMaxThreads
主要用于限制程序无限制的创造线程导致的灾难。目的是让程序在干掉操作系统之前,先干掉它自己。
- P和M的关系,就好比用户线程和内核线程的
goroutine
是按照抢占式调度的,一个goroutine
最多执行10ms就会换作下一个;