进程,线程和协程概念
进程
- 进程是程序在一个数据集上的一次运行过程。
- 进程是操作系统进行资源分配的基本单位。
- 每个进程都有自己的独立内存空间,不同进程通过进程间同步信号量来通信。
- 由于进程比较重量,占据独立的内存,所以上下文进程间的切换开销(栈、寄存器、虚拟内存、文件句柄等)比较大,但相对比较稳定安全。
线程
- 线程是进程中的一个实体,是被操作系统进行
CPU
调度和执行的基本单位。 - 一个进程包含一个或多个线程;它是比进程更小的能独立运行的基本单位.;线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈)。
- 一个线程与同属一个进程的其他的线程共享进程所拥有的全部资源。
- 线程间通信主要通过共享内存,上下文切换很快,资源开销较少,但相比进程不够稳定容易丢失数据。
协程
- 协程是一种用户态的轻量级线程,协程的调度完全由用户控制。协程拥有自己的寄存器上下文和栈。
- 协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈,直接操作栈则基本没有内核切换的开销,可以不加锁的访问全局变量,所以上下文的切换非常快。
进程、线程 和 协程区别
- 进程、线程,都是内核进行调度,CPU采用的是时间片轮换+优先级的抢占式调度(有多种调度算法)
- 协程(用户级轻量级线程),对内核透明的,也就是系统并不知道有协程的存在,是完全由用户自己的程序进行调度的,它不像抢占式调度强制
CPU
交出控制权,切换到其他进程/线程,通常只能进行协作式调度,需要协程自己主动把控制权转让出去之后,其他协程才能被执行到。
goroutine 和协程
- 本质上,
goroutine
就是协程。 不同的是,Golang
在runtime
、系统调用等多方面对goroutine
调度进行了封装和处理,当遇到长时间执行或者进行系统调用时,会主动把当前goroutine
的CPU (P)
转让出去,让其他goroutine
能被调度并执行,也就是Golang
从语言层面支持了协程。 Golang
的一大特色就是从语言层面原生支持协程,在函数或方法前面加go
关键字就可创建一个协程。
内存消耗
-
每个
-
goroutine
:2KB - 线程:8MB
goroutine
(协程) 默认占用内存远比
Java
、
C
的
线程少。
线程和 goroutine
调度开销
-
线程/
-
线程:涉及模式切换(从用户态切换到内核态)、
16
个寄存器、PC
、SP
(段寄存器)…等寄存器的刷新。 -
goroutine
:只有三个 寄存器的值修改 -PC
/SP
/DX
。
goroutine
切换开销方面,
goroutine
远比线程小。
AX(累加器),BX(基址寄存器),CX(计数寄存器),DX(数据寄存器),SP(堆栈指针),BP(基址指针),SI(源变址指针),DI(目的变址指针),IP(指令指针),CS(代码段寄存器),DS(数据段寄存器),SS(堆栈寄存器),ES(附加段寄存器),PC(程序计数寄存器),LR( 链接寄存器)
协程底层实现原理
线程是操作系统的内核对象,多线程编程时,如果线程数过多,就会导致频繁的上下文切换,这些 CPU
时间是一个额外的耗费。所以在一些高并发的网络服务器编程中,使用一个线程服务一个 socket
连接是很不明智的。于是操作系统提供了基于事件模式的异步非阻塞编程模型。用少量的线程来服务大量的 socket
网络连接和I/O
操作。但是采用异步和基于事件的编程模型,复杂化了程序代码的编写,非常容易出错。因为线程穿插,也提高排查错误的难度。
协程,是在应用层模拟的线程,它避免了上下文切换的额外耗费,兼顾了多线程的优点。简化了高并发程序的复杂度。举个例子,一个高并发的网络服务器,每一个socket
连接进来,服务器用一个协程来对他进行服务。代码非常清晰。而且兼顾了性能。
协程调度
协程和线程的原理是一样的,当A
线程切换到B
线程的时候,需要进入内核态将A
线程的相关执行进度压栈,然后将B
线程的执行进度出栈,进入B
线程的执行序列;协程只不过是在用户态实现了这一点。
Q:协程并不是由操作系统调度的,而用户态应用程序也没有能力和权限执行CPU
调度。怎么解决这个问题?
-
协程是基于线程的。内部实现上,维护了一组数据结构(
P
上下文环境)和N
个线程(M
一个M
对应一个内核级线程),真正的执行还是线程,协程(G goroutine
)执行的代码被扔进N+1
个待执行队列中,由这N
个线程从不同的队列中调度取出来执行。这就解决了协程的执行问题。 -
Golang
对各种I/O
函数 进行了封装,将这些封装的函数提供给应用程序使用,而其内部调用了操作系统的异步I/O
函数,当这些异步函数返回busy
或bloking
时,Golang
利用这个时机将现有的执行序列压栈,让线程去队列中取出另外一个协程(goroutine
)的代码来执行,基本原理就是这样,利用并封装了操作系统的异步函数。包括Linux
的epoll
、select
和Windows
的iocp
、event
等。
基于抢占式的协作调度
我们知道与用户级线程M(物理处理器,runtime运行时系统内核调度实体),会对应一个内核级线程,调度方式肯定是CPU抢占式调度。
多个G(goroutine),被挂在一个逻辑处理器P上。然后由p负责把那个G,送给M执行。
这样的设计就避免了,好多线程,切换导致cpu利用率低的问题。所有G执行都在一个M上进行,M执行等待时间大大减小。
那么问题来了goroutine执行方式不是非抢占式的吗?怎么体现啊。
Golang的运行时runtime系统,调度逻辑处理器P,协作调度G到M上运行,是非抢占式的。(它没有时间片,但是为了避免某些goroutine等时过长产生饥饿,会有超时调度,属于初级抢占式)
调度过程
Go程序的初始化,runtime创建一条后台线程,运行一个sysmon函数。这个函数会周期性地做epoll操作,同时它还会检测每个P是否运行了较长时间。
如果检测到某个P状态处于Psyscall超过了一个sysmon的时间周期(20us),并且还有其它可运行的任务,则调度切换P。
如果检测到某个P的状态为Prunning,并且它已经运行了超过10ms,则会将P的当前的G的stackguard设置为StackPreempt。相当于做了一个标记,通知这个G在合适时机进行调度。
Go使用的是分段栈,它会在每个函数入口处比较当