引言
关于这篇文章的由来, 起初是近期在搬一些老文章过来, 看到了之前写的内容。但随着工作时间久了, 对这些东西又有了些不一样的看法, 所以就重新记录下来。纯是作为个人分享记录, 如果有不对的地方, 希望大家指出。
一、进程、线程、协程
进程
进程是操作系统进行资源分配和调度的基本单位。每个进程拥有独立的内存空间、系统资源, 进程间的通信需要通过特定的IPC机制(如管道、消息队列、共享内存等)。
线程
线程是 CPU 调度的基本执行单位,是进程内的一个执行流。同一进程的多个线程共享进程的内存和资源,但各自拥有独立的栈空间。
协程
协程是用户态的轻量级线程,由程序控制调度,可以在单个线程内实现协作式多任务。协程的挂起和恢复由程序显式控制,切换开销极小。
区别与联系
老生常谈的基本定义如上, 下面我们来说些人话, 聊一聊它们到底是个什么东西。
抽象一点:
我们将整个计算机系统比做一套豪华公寓, 每个 CPU 核心比做一个单独的卧室
-
进程 - 独享整套公寓,有独立客厅厨房(独立内存空间),换人需要重新布置所有地方
-
线程 - 合租室友,共享客厅厨房(共享内存),各有卧室(独立栈),换人只需换卧室
-
协程 - 同一卧室里轮流使用房间的人,自觉记录进度轮流使用,极致高效但需要配合
特点对比:
| 特性 | 进程 | 线程 | 协程 |
|---|---|---|---|
| 资源开销 | 大 | 中等 | 极小 |
| 切换成本 | 高 | 中 | 低 |
| 通信方式 | IPC机制 | 共享内存 | 全局变量/队列 |
| 隔离性 | 强(相互隔离) | 弱(共享内存地址) | 极弱(共享线程资源) |
| 调度方式 | 操作系统抢占式 | 操作系统抢占式 | 用户协作式 |
总结:
- 进程:环境默认隔离(但是可以通过管道、消息队列等形式实现通信), 安全性很高, 缺点是资源开销很大。
- 线程:线程共享进程资源, 但也会相互影响, 如某一个线程搞崩环境, 所有线程都会受到影响。
- 协程:虽然说是"用户控制", 但在日常编程中, 通常由运行时库自动调度,开发者只需使用
async/await等关键字。 需要注意的是, 因为共用一个线程, 如果一个任务持续占用 CPU 等资源, 所有的任务都会被阻塞。
二、如何选择
我们日常处理的任务主要分为两类:计算密集型(如数值计算、图像处理)和 IO密集型(如网络请求、文件读写)。
- 计算密集型任务:主要消耗 CPU 计算资源, 需要进行大量数学运算
- IO密集型任务:主要时间花费在等待 IO 操作完成, CPU 经常处于空闲状态
那么我们应该如何根据任务进行选择呢?
- 首先判断任务类型, 如果全部为 IO 密集型任务, 协程便是最优解。
- 如果是混合型任务或者轻度计算任务, 且需要共享资源, 多线程便是最优解。
- 如果是计算密集型任务, 且需要任务间互相独立, 互不影响(不会因为一个任务挂了影响其他任务), 那么多进程是最优解。
举例说明
// 情况一, 爬虫任务, 有一百个网站需要同时发起请求进行爬取。
毫无疑问, 全部是 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 进行任务切换, 都恰好在不同的进程间进行切换, 这时多线程并不会比多进程更加省资源(但是仅这一种情况下, 概率很低)。
同一个任务多线程大概率比多进程更加节省切换资源, 简单任务多采用多线程(同一进程内), 线程在切换时就更容易在同一进程内, 更加节省资源。