关于进程、线程、协程的看法和理解

109 阅读8分钟

引言

关于这篇文章的由来, 起初是近期在搬一些老文章过来, 看到了之前写的内容。但随着工作时间久了, 对这些东西又有了些不一样的看法, 所以就重新记录下来。纯是作为个人分享记录, 如果有不对的地方, 希望大家指出。

一、进程、线程、协程

进程

进程是操作系统进行资源分配和调度的基本单位。每个进程拥有独立的内存空间、系统资源, 进程间的通信需要通过特定的IPC机制(如管道、消息队列、共享内存等)。

线程

线程是 CPU 调度的基本执行单位,是进程内的一个执行流。同一进程的多个线程共享进程的内存和资源,但各自拥有独立的栈空间。

协程

协程是用户态的轻量级线程,由程序控制调度,可以在单个线程内实现协作式多任务。协程的挂起和恢复由程序显式控制,切换开销极小。

区别与联系

老生常谈的基本定义如上, 下面我们来说些人话, 聊一聊它们到底是个什么东西。

抽象一点:

我们将整个计算机系统比做一套豪华公寓, 每个 CPU 核心比做一个单独的卧室

  • 进程 - 独享整套公寓,有独立客厅厨房(独立内存空间),换人需要重新布置所有地方

  • 线程 - 合租室友,共享客厅厨房(共享内存),各有卧室(独立栈),换人只需换卧室

  • 协程 - 同一卧室里轮流使用房间的人,自觉记录进度轮流使用,极致高效但需要配合

特点对比:

特性进程线程协程
资源开销中等极小
切换成本
通信方式IPC机制共享内存全局变量/队列
隔离性强(相互隔离)弱(共享内存地址)极弱(共享线程资源)
调度方式操作系统抢占式操作系统抢占式用户协作式

总结:

  • 进程:环境默认隔离(但是可以通过管道、消息队列等形式实现通信), 安全性很高, 缺点是资源开销很大。
  • 线程:线程共享进程资源, 但也会相互影响, 如某一个线程搞崩环境, 所有线程都会受到影响。
  • 协程:虽然说是"用户控制", 但在日常编程中, 通常由运行时库自动调度,开发者只需使用async/await等关键字。 需要注意的是, 因为共用一个线程, 如果一个任务持续占用 CPU 等资源, 所有的任务都会被阻塞。

二、如何选择

我们日常处理的任务主要分为两类:计算密集型(如数值计算、图像处理)和 IO密集型(如网络请求、文件读写)。

  • 计算密集型任务:主要消耗 CPU 计算资源, 需要进行大量数学运算
  • IO密集型任务:主要时间花费在等待 IO 操作完成, CPU 经常处于空闲状态

那么我们应该如何根据任务进行选择呢?

  1. 首先判断任务类型, 如果全部为 IO 密集型任务, 协程便是最优解。
  2. 如果是混合型任务或者轻度计算任务, 且需要共享资源, 多线程便是最优解。
  3. 如果是计算密集型任务, 且需要任务间互相独立, 互不影响(不会因为一个任务挂了影响其他任务), 那么多进程是最优解。

举例说明

// 情况一, 爬虫任务, 有一百个网站需要同时发起请求进行爬取。
毫无疑问, 全部是 io 密集型任务, 不需要 cpu 资源, 协程就是最好的选择。

// 情况二, 爬虫爬取图片, 然后更改图片分辨率, 下载到本地
典型的混合型任务, 需要一定的 cpu 资源, 这时候多线程是最合适的。
如果选用协程, 在计算图片的时候占用 cpu 资源, 会导致整体任务发生阻塞, 即计算图片这一部分就成为了同步。

// 情况三, 爬虫爬取图片, 对图片进行复杂计算, 下载到本地
虽然和情况二相同是混合型任务, 但是这里是复杂型计算, 需要长时间占用 cpu 造成阻塞, 所以最好的形式应该是如下情况。
为爬虫和下载分别开一个单独的线程任务(里边开协程进行), 计算任务单独开线程来做, 这样就不会因为计算任务而阻塞其他两个任务, 对于这种纯 cpu 计算的任务, 线程数最佳为和 cpu 核数保持一致。
这个情况下计算任务不需要共享内存, 当计算任务过重时甚至可以选择为计算任务开多进程来做, 反而不会因为某个计算任务出问题而导致整体任务失败。

三、Python 的 GIL 锁问题

GIL 锁要求 Python 在同一时刻只允许一个线程在运行, 所以实际上 Python 是没有真正的并行情况的。

假设有 10 个计算密集型任务交由 Python 执行。

  • 协程, 真正的单线程执行, 基本等于同步, 任务一个接一个完成。
  • 多线程, 将 10 个任务分发给 10 个线程, 根据轮转所有任务交替执行, 每个任务都有进度, 基本在最后同时完成, 但是因为需要切换线程的原因, 整体开销会大于协程。

那么什么时候会选用多线程?

  • 第三方库兼容, 一些库进行的是 IO 等待却并不兼容协程的形式, 可以选择多线程进行一种"妥协"的方法。
  • 一些 C 扩展库, 会在库中对 GIL 锁进行操作, 实现真正的多线程。
  • 一些 GUI 框架的约束, 要求主线程必须是 UI 线程。
  • 一些用户交互优化时, 多线程可以让每个任务的进度都在动。

其余情况下尽可能选用多进程或是协程。

四、一些其他问题

以上内容基本可以覆盖多数问题, 如果还想更深层次的理解, 可以一起探讨下边的问题。

既然说线程是 cpu 调度的基本单位, 那么我们说的"进程切换"到底是什么?

从本质上来说, cpu 的调度是以线程为单位的, 也就是说仅仅存在线程的切换。

在创建一个进程时, 操作系统会为该进程分配一部分资源, 在该进程下的所有线程会共享该部分资源。当在这些线程中进行切换操作时, 只需要在同一个"资源块"中进行切换, 而如果跨进程切换线程, 则需要先切换"资源块", 再完成线程的切换, 导致消耗更多的资源。

一个很好的比喻是:

  • 进程就像一个公司, 拥有办公场地、银行账户、知识产权等资源。
  • 线程就像公司的员工, 在公司的资源环境下执行具体任务。
  • 线程切换, 指给员工委派不同的任务。
  • 进程切换, 给员工委派其他公司的任务, 需要先获取其他公司的各种资源后才能开始任务, 消耗更多。

即是说进程切换仅是线程切换的一种特殊情况。

在实际的 cpu 调度中, 同一进程内的线程会放在进行调度吗?

在通用操作系统中,线程切换的顺序和时机本质上是不可控的,具有很强的不确定性。

通常情况下, 线程的调度是抢占式的, 一个正在运行的线程随时可能被操作系统中断,而无需它自己主动放弃CPU。但是同时他也会受到一些优先级策略、调度策略和硬件因素影响。

总体来说, 线程的切换顺序是不可控的, 同一进程内的线程也不一定会在一起进行调度。

多线程一定比多进程更加省资源吗?

根据上述问题可知, 线程的调度是不会因为进程关系而改变的。他的调度顺序是不可控的。

如果说每次 CPU 进行任务切换, 都恰好在不同的进程间进行切换, 这时多线程并不会比多进程更加省资源(但是仅这一种情况下, 概率很低)。

同一个任务多线程大概率比多进程更加节省切换资源, 简单任务多采用多线程(同一进程内), 线程在切换时就更容易在同一进程内, 更加节省资源。