架构师之路011
多任务 进程 线程 协程
多任务的需求是随处可见的
物理层面来说 最早的cpu 都是单核的 同一时间只能执行一条指令
两个方法
- 多颗cpu
- 单核cpu多个核心
在桌面端 大多数情况是后者
在服务器端领域 通常同时使用两者 关注的是如何尽可能提升单台计算机的计算力密度
把cpu的时间切成一段时间片 每个时间片只运行某一个软件 因为时间片很小 所以会感觉这些软件都在同时运行 这种分时间片的叫做分时系统
分时系统 把当前任务状态保存起来 把另一个任务的状态恢复 把执行权交给它即可
涉及的问题有:
- 任务是什么 如何抽象
- 任务的状态有哪些 如何保存和恢复
- 什么时机会发生任务切换
大部分的操作系统提供了两种任务的抽象 进程和线程
部分操作系统提供了第三套 叫做协程(也叫纤程)
执行体 是指可被cpu赋予执行权的对象 至少包含下一个执行位置以及其他的运行状态
任务的状态 都有什么?
从cpu的角度 执行程序主要依赖的是内置存储 寄存器和内存ram 构成执行体的上下文
寄存器 数量很少且可以枚举 直接通过寄存器名称进行数据的存取
用到的寄存器保存起来 再恢复到软件B上一次执行时的值 然后把执行权交给软件B
从外面的视角来看 好像一直是独立运行未受到打扰
内存ram
在实模式下 多个执行体同在一个内存地址空间 相互并无干扰
在保护模式下 不同任务可以有不同的地址空间 通过不同的地址映射来体现 切换地址映射表 也是寄存器
总结就是一句话
执行体的上下文 就是一堆寄存器的值 要切换执行体 只需要保存和恢复一堆寄存器的值即可
进程和线程
进程是操作系统从安全角度来说的隔离单位 不同进程之间基于最低授权的原则
在创建进程这个事情上 unix 直接使用的fork 使用上便利 但是最系统设计中最差的api 没有之一
是过度设计 甚至可以说是设计事故
线程的设计 是因为操作系统发现 同一个软件内部 还是有多任务的需求 在相同的地址空间 彼此之间可以信任
进程实际上承担了一部分来自线程的需求 需要父进程的环境
协程并不是操作系统内核提供的 用户态线程
如果感兴趣 你也可以自己实现一个
实现高性能的网络服务器的需要
对于普通的桌面程序而言 进程加上线程绰绰有余
网络服务器 大量的来自客户端的请求和服务器的返回包 都是网络io;在响应请求的过程中 往往需要访问存储来保存和读取自身的状态 这也涉及到本地或者网络io
大量并行的io请求
开销成本
- 系统调用机制产生的开销
- 数据多次拷贝的开销
- 没有数据而阻塞 产生调度重新获得执行权 产生的时间成本
- 线程的空间成本和时间成本
改善网络服务器的吞吐能力 主流是epoll
在io时登记一个io请求 然后统一在某个线程查询谁的io先完成了 谁先完成了让谁处理
从系统调用的角度来说 epoll 产生了更多次数的系统调用 从内存拷贝也没有减少 真正有意义的事情是 减少了线程的数量
异步回调反人类
时间成本的拆解
- 执行体切换本身的开销 寄存器保存和恢复的成本
- 执行体的调度开销
- 执行体之间的同步和互斥成本
空间成本
- 执行体的执行状态
- TLS(线程局部存储)
- 执行体的堆栈
协程的目的
- 回归到同步io的编程模式
- 降低执行体的空间成本和时间成本
大部分的协程库 缺失很多内容
- 协程的调度
- 协程的同步 互斥 通讯
- 协程的系统调用包装 尤其是网络io请求的包装
协程的堆栈是一个难题 太小不够用 太大则协程的空间成本过高 影响能够处理的网络请求的并发数
理想情况下 需要自动进行调整
完备的协程库
erlang和golang
重要设计
- 堆栈开始很小4kb 可以按需自动增长
- 坚决干掉了线程局部存储的特性支持 执行体更精简
- 提供了同步 互斥和其他常规执行体的通讯手段 包括channel
- 提供了所有重要的系统调用
操作系统内核是非常庞大而且复杂的基础软件 设计需要极强的预见性
体会精妙思想的同时 批判进行吸收