多线程与多进程的基本原理

在一台计算机中,我们可以同时打开多个软件,例如同时测览网页、听音乐、打字等,这是再正常不过的事情。但仔细想想,为什么计算机可以同时运行这么多软件呢?这就涉及计算机中的两个名词:多进程和多线程。

同样,在编写爬虫程序的时候,为了提高爬取效率,我们可能会同时运行多个爬虫任务,其中同样涉及多进程和多线程。

多线程的含义

说起多线程,就不得不先说什么是线程。说起线程,又不得不先说什么是进程。

进程可以理解为一个可以独立运行的程序单位,例如打开一个浏览器,就开启了一个浏览器进程;打开一个文本编辑器,就开启了一个文本编辑器进程。在一个进程中,可以同时处理很多事情,例如在浏览器进程中,可以在多个选项卡中打开多个页面,有的页面播放音乐,有的页面播放视频,有的网页播放动画,这些任务可以同时运行,互不干扰。为什么能做到同时运行这么多任务呢?这便引出了线程的概念,其实一个任务就对应一个线程。

进程就是线程的集合,进程是由一个或多个线程构成的,线程是操作系统进行运算调度的最小单位,是进程中的最小运行单元。以上面说的浏览器进程为例,其中的播放音乐就是一个线程,播放视频也是一个线程。当然,浏览器进程中还有很多其他线程在同时运行,这些线程并发或并行执行使得整个浏览器可以同时运行多个任务。

了解了线程的概念,多线程就很容易理解了。多线程就是一个进程中同时执行多个线程,上面的 浏览器进程就是典型的多线程。

并发和并行

说到多进程和多线程,不得不再介绍两个名词一一并发和并行。我们知道,在计算机中运行一个程序,底层是通过处理器运行一条条指令来实现的。

处理器同一时刻只能执行一条指令,并发(concurrency)是指多个线程对应的多条指令被快速轮换地执行。例如一个处理器,它先执行线程A的指令一段时间,再执行线程B的指令一段时间,然后再切回线程A执行一段时间。处理器执行指令的速度和切换线程的速度都非常快,人完全感知不到计算机在这个过程中还切换了多个线程的上下文,这使得多个线程从宏观上看起来是同时在运行。从微观上看,处理器连续不断地在多个线程之间切换和执行,每个线程的执行都一定会占用这个处理器的一个时间片段,因此同一时刻其实只有一个线程被执行。

并行(parallel)指同一时刻有多条指令在多个处理器上同时执行,这意味着并行必须依赖多个处理器。不论是从宏观还是微观上看,多个线程都是在同一时刻一起执行的。

并行只能存在于多处理器系统中,因此如果计算机处理器只有一个核,就不可能实现并行。而并发在单处理器和多处理器系统中都可以存在,因为仅靠一个核,就可以实现并发。

例如,系统处理器需要同时运行多个线程。如果系统处理器只有一个核,那它只能通过并发的方 式来运行这些线程。而如果系统处理器有多个核,那么在一个核执行一个线程的同时,另一个核可以执行另一个线程,这样这两个线程就实现了并行执行。当然,其他线程也可能和另外的线程在同一个核上执行,它们之间就是并发执行。具体的执行方式,取决于操作系统如何调度。

多线程适用场景

在一个程序的进程中,有一些操作是比较耗时或者需要等待的,例如等待数据库查询结果的返回、等待网页的响应。这时如果使用单线程,处理器必须等这些操作完成之后才能继续执行其他操作,但在这个等待的过程中,处理器明显可以去执行其他操作。如果使用多线程,处理器就可以在某个线程处于等待态的时候,去执行其他线程,从而提高整体的执行效率。

很多情况和上述场景一样,线程在执行过程中需要等待。网络爬虫就是一个非常典型的例子,爬虫在向服务器发起请求之后,有一段时间必须等待服务器返回响应,这种任务就属于I0密集型任务。对于这种任务,如果我们启用多线程,那么处理器就可以在某个线程等待的时候去处理其他线程,从而提高整体的爬取效率。

但并不是所有任务都属于IO密集型任务,还有一种任务叫作计算密集型任务,也可以称为CPU密集型任务。顾名思义,就是任务的运行一直需要处理器的参与。假设我们开启了多线程,处理器从一个计算密集型任务切换到另一个计算密集型任务,那么处理器将不会停下来,而是始终忙于计算,这样并不会节省整体的时间,因为需要处理的任务的计算总量是不变的。此时要是线程数目过多,反而还会在线程切换的过程中耗费更多时间,使得整体效率变低。

综上所述,如果任务不全是计算密集型任务,就可以使用多线程来提高程序整体的执行效率。尤其对于网络爬虫这种10密集型任务,使用多线程能够大大提高程序整体的爬取效率。

多进程的含义

前文我们已经了解了进程的基本概念,进程(process)是具有一定独立功能的程序在某个数据集合上的一次运行活动,是系统进行资源分配和调度的一个独立单位。

顾名思义,多进程就是同时运行多个进程。由于进程就是线程的集合,而且进程是由一个或多个线程构成的,所以多进程意味着有大于等于进程数量的线程在同时运行。

Python 的多线程和多进程

Python 中 GIL 的限制导致不论是在单核还是多核条件下,同一时刻都只能运行一个线程这使得 Python 多线程无法发挥多核并行的优势。

GIL 全称为 Global Interpreter Lock,意思是全局解释器锁,其设计之初是出于对数据安全的考虑。

在 Python 多线程下,每个线程的执行方式分如下三步。

  • 获取 GIL。

  • 执行对应线程的代码。

  • 释放 GIL。

可见,某个线程要想执行,必须先拿到 GIL。我们可以把 GIL 看作通行证,并且在一个 Python 进程中,GIL 只有一个。线程要是拿不到通行证,就不允许执行。这样会导致即使在多核条件下,一个 Python 进程中的多个线程在同一时刻也只能执行一个。

而对于多进程来说,每个进程都有属于自己的 GIL,所以在多核处理器下,多进程的运行是不会受 GIL 影响的。也就是说,多进程能够更好地发挥多核优势。

不过,对于爬虫这种 IO 密集型任务来说,多线程和多进程产生的影响差别并不大。但对于计算密集型任务来说,由于 GIL 的存在,Python 多线程的整体运行效率在多核情况下可能反而比单核更低。

而 Python 的多进程相比多线程,运行效率在多核情况下比单核会有成倍提升。

从整体来看,Python 的多进程比多线程更有优势。所以,如果条件允许的话,尽量用多进程。

值得注意的是,由于进程是系统进行资源分配和调度的一个独立单位,所以各进程之间的数据是无法共享的,如多个进程无法共享一个全局变量,进程之间的数据共享需要由单独的机制来实现。

关于 Python 中多进程和多线程的具体用法,由于篇幅原因,这里不再展开介绍,请移步如下链接进行学习。

总结

本节介绍了多线程、多进程的基本知识,如果我们可以把多线程、多进程运用到爬虫中的话,爬虫的爬取效率将会大幅提升。

由于涉及一些专业名词,本节内容参考如下资料。

  • 百度百科上多线程、多进程相关的内容。

  • Python 官方文档中 threading 相关的内容。

  • Python 官方文档中 multiprocessing 相关的内容。

  • 博客园网站上的 “多进程和多线程的概念” 文章。