进程、线程、协程
为了满足多任务需求,人们发明了三套执行体:进程、线程、协程,以及各种执行体之间的通信机制。
分时系统
操作系统需要执行多任务模式,有两个方法:
- 提升CPU颗数
- 提升CPU核心数
对于物理硬件来说,我们通常希望尽可能设备体积尽可能小,因此现代计算机通常CPU颗数不多,但是核数较多
如果是单个CPU的单核要实现多任务,则需要需要操作系统切分多个时间片进行分时处理
执行体
分时系统在执行任务时需要将当前任务挂起,恢复另一个任务并移交执行权限,这个过程有几个问题需要解决:
- 任务是什么?由什么构成?
- 任务状态有哪些,任务是怎么被保存和恢复的?
- 什么时刻需要进行任务切换?
操作系统层面提供了两种任务机制,进程和线程,任务至少包含两个要素:
- 下一个执行位置
- 自身状态
保存这两个关键信息的介质是:
- 寄存器:上下文切换时,保存当前时刻寄存器的值,将寄存器值置为下一个任务挂起时的状态,继而开始执行下一个任务
- RAM(内存):每个进程都有自己的内存空间,任务切换时需要找到自己的虚拟内存映射表,映射表同样是从寄存器读取
总结:
- 上下文 = 寄存器中的值
- 上下文切换 = 寄存器值保存、恢复过程
进程
进程的出现源于计算机同时运行多个软件的需求。
1、系统提供的一种任务隔离单元,不同进程之间资源相互隔离
2、fork模式是一种进程间通信的特殊方式:子进程复制副进程,之后父子进程各干各的事
3、进程的同步、互斥、通信
- 竞态条件:不同执行体需要同时访问操作同一资源时,执行结果依赖于操作时序
- 互斥、临界区:将一段代码定义为临界区,阻止不同执行体同时进入临界区,这种行为叫做互斥
- 信号量:执行体进入临界区之前获取一个令牌,成功则进入,否则等待其他执行体释放令牌
- 互斥量(锁):mutex互斥体,lock -> do something -> unlock
线程
在同一个软件内同样存在多任务需求。
- 共享进程资源空间,彼此之间相互信任
- 早期Linux系统不存在线程,因此进程需要承担一部分线程的功能,因此可能才有了fork模式
协程
对于海量并发场景,直接使用系统多线程会存在这些成本问题:
1、时间成本
- 执行体切换(寄存器保存和恢复),腾挪余地有限
- 执行体调度的开销,成本随线程数量线性升高
- 执行体之间的同步和互斥成本
2、空间成本
- 执行体状态保存
- 执行体堆栈
- 线程局部存储(TLS)
为了解决这些问题,以golang为代表的编程语言在用户态实现了协程机制:
- 堆栈初始大小仅4k,可以按需扩容,最高可达百万并发级别
- 提供了channel的同步和互斥机制
- 实现了同步IO+线程池的包装(GMP),将大量并发包装到了有限数量的线程中
通信机制
这里整理一下广义的进程通信机制。
