定义
- 进程是系统进行资源分配的基本单位, 是操作系统结构的基础, 它的执行需要系统分配资源创建实体后, 才能进行
- 线程不能够也不需要单独分配资源, 是调度的基本单位, 多线程相比多进程可以节省时间和资源开销
相同点
- 对于开发者而言, 都是实现多任务并发的手段, 都可以独立调度, 在多任务环境下, 功能层面无差异
- 二者都具有实体, 是系统独立管理的对象个体, 在系统层面都可以实现对二者的控制
- 一般来说, 子进程/子线程的调度与父进程/父线程平等竞争
- 状态非常相似
实现方式的差异
- 进程的个体间是完全独立的, 多进程环境中, 任何一个进程的终止不会影响其他进程
- 多线程环境中, 父线程终止, 全部子线程被迫终止(资源回收), 但任何一个子线程中止, 不会影响其他线程, 除非子线程执行了exit(), 会导致全部线程同时终止
- 从系统实现角度来讲, 进程是通过 fork 系统调用, 线程是通过 clone 系统调用
- fork() 是将全部资源复制给子进程, 而 clone 只复制了小部分必要资源, 并且调用 clone 时可以通过参数控制要复制的对象, 可以将 fork 理解为 clone 的完整加强版
- fork() 的优化 – 写时复制技术, 需要时才复制, 创建时先不复制
通信方式
- 进程间通信 – IPC (共 8 种)
- 共享内存
- 消息队列
- 信号量
- 有名管道
- 无名管道
- 信号
- 文件
- Socket
- 线程间通信( 所有进程通信方式都可以使用, 共 13 种)
- 互斥量
- 自旋锁
- 条件变量
- 读写锁
- 信号(进程使用进程信号, 线程使用线程信号)
- 全局变量
- 进程通信要么切换内核上下文, 要么访问外设, 都影响速度
- 线程通信都在自己进程空间内, 不存在切换, 所以速度较快
使用场景
-
多进程
- nginx 主流的工作模式, 多进程单线程
主进程执行 accept() 接收连接请求, 一个用户进来, fork()一个子进程用来处理请求. 因为子进程会将 val 复制到自己的空间, 所以父进程不需要复制 val 执行 accept(), 可以用同一 val 保存 accept() 的返回值, 即使父进程覆盖之前的值也互不影响 - web server
- redis也可以归类到“多进程单线程”模型(平时工作是单个进程,涉及到耗时操作如持久化或aof重写时会用到多个进程)
- nginx 主流的工作模式, 多进程单线程
-
多线程
- 任务间有数据共享, 并且需要修改
- 需要大量 CPU 计算, 切换频繁, 多线程可以减少切换开销
状态 和 上下文切换
- 三种状态: 运行, 阻塞, 就绪
- 上下文切换会发生在阻塞状态, 如IO等待或网络等待
- 进程和线程的切换都是根据 os 策略由操作系统执行, 协程的切换由程序执行, 时机由程序决定, 用户都无感
- 进程的切换内容包括全局目录, 内核栈, 硬件上下文, 内容保存在内存中, 用户态->内核态->用户态, 效率低
- 线程的切换内容包括内核栈, 硬件上下文, 内容保存在内核栈中, 用户态->内核态->用户态, 效率比进程切换高
- 协程的切换内容包括上下文, 保存在用户栈中, 切换过程不设计内核态, 效率高
- 通常来说进程的切换时机
- io 或者网络等待
- 优先级更高的进程抢占 cpu 资源
资源回收机制
- 一般情况下, 主进程会等子进程执行结束后再结束, 主进程回收资源
- 如果主进程先结束, 子进程会成为孤儿进程被 init 进程"收养", 等待其结束后, 资源被 init 进程回收
- 如果子进程结束, 但父进程未回收其资源, 则子进程此时为僵尸进程, 处理方法: 将僵尸进程的父进程kill, 使僵尸进程变为孤儿进程, 资源被 init 回收
多进程和多线程数
- 多进程推荐为 cpu 核心数, 多线程推荐为 cpu 核心数*5, 协程数量为线程数 * 500
- 协程是串行的, 一个线程可以拥有大概 500 个协程