什么是多线程/多进程
引用虫师的解释:
计算机程序只不过是磁盘中可执行的,二进制(或其它类型)的数据。它们只有在被读取到内存中,被操作系统调用的时候才开始它们的生命期。
进程(有时被称为重量级进程)是程序的一次执行。每个进程都有自己的地址空间,内存,数据栈以及其它记录其运行轨迹的辅助数据。操作系统管理在其上运行的所有进程,并为这些进程公平地分配时间。
线程(有时被称为轻量级进程)跟进程有些相似,不同的是,所有的线程运行在同一个进程中,共享相同的运行环境。我们可以想像成是在主进程或“主线程”中并行运行的“迷你进程”。
为什么需要多线程/多进程
我们直接编写的爬虫程序是单线程的,在数据需求量不大时它能够满足我们的需求。
但如果数据量很大,比如要通过访问数百数千个url去爬取数据,单线程必须等待当前url访问完毕并且数据提取保存完成后才可以对下一个url进行操作,一次只能对一个url进行操作;
我们使用多线程/多进程的话,就可以实现对多个url同时进行操作。这样就能大大缩减了爬虫运行时间。
实现多线程/多进程
多线程
python提供了两组多线程接口,一是thread模块_thread,提供低等级接口;二是threading模块,在thread模块基础上进行封装,提供更容易使用的基于对象的接口,可以继承Thread对象来实现多线程。
同时,还有其他线程相关的对象,如Timer、Lock等。
在这里,我们使用threading模块实现多线程。
1. 添加线程
threading.Thread(target, args)
使用threading.Thread()新建一个线程,target是需要执行的函数,args是需要传入该函数的参数,args接受一个tuple,即使只有一个参数也需要写成(x,)形式
2. 线程阻塞:join
join()的作用是调用该线程时,等待该线程完成后再继续往下运行。
join通常用于主线程与子线程之间,主线程等待子线程运行完毕后再继续执行,避免子程序和主程序同时运行,子程序还没有运行完的时候主程序就已经运行结束。
3. 信息传递:Queue队列
Queue是python标准库中的线程安全的队列(FIFO)实现,提供了一个适用于多线程编程的先进先出的数据结构,即队列。
Queue是一种先进先出的数据结构,一般来说读数据都从Queue头读,写数据都从Queue尾写入。
4. 线程锁:Lock
lock在不同线程使用同一共享内存时,能够确保线程之间互不影响。
使用lock的方法是:在每个线程执行运算修改共享内存之前执行lock.acquire()将共享内存上锁, 确保当前线程执行时,内存不会被其他线程访问;
执行运算完毕后使用lock.release()将锁打开, 保证其他的线程可以使用该共享内存。
lock.acquire()和lock.release()必须成对出现。
将上述代码中的lock.acquire()和lock.release()四行代码注释后运行,就是不加锁的情况,这时候输出结果都是混乱的。而加锁后,输出结果正常。
5. 线程池
线程池有几种方法可以实现,这里我们使用multiprocessing.dummy库。
多进程
多进程multiprocessing和多线程threading类似,都是用在python中进行并行计算的,而多进程则是为了弥补python在多线程中的劣势而出现的。
multiprocessing是使用计算机的多核进行运算,它可以避免多线程中GIL的影响。
python使用multiprocessing模块实现多进程,用法和threading基本一致。
1. 添加进程
multiprocessing.Process(target, args)
使用multiprocessing.Process新建一个进程,target是需要执行的函数,args是需要传入该函数的参数,args接受一个tuple,即使只有一个参数也需要写成(x,)形式
2. 信息传递:Queue队列
多进程中的Queue使用同多线程一致,同样为先进先出。
多进程可以直接从multiprocessing.Queue()导入Queue队列。
3. 进程池
关于进程池的更多信息请跳转至:
4. 共享内存
一般的变量在进程之间是没法进行通讯的,multiprocessing 给我们提供了 Value 和 Array 模块,他们可以在不通的进程中共同使用。
Value()和Array()都接受两个参数,第一个为数据类型,第二个是传入的数。
Value()可以接受传入单个数值,Array()可以接受传入一个一维数组。
数据类型如下:
Type code
C Type
Python Type
Minimum size in bytes
'b'
signed char
int
1
'B'
unsigned char
int
1
'u'
py_UNICODE
Unicode character
2
'h'
signed short
int
2
'H'
unsigned short
int
2
'i'
signed int
int
2
'I'
unsigned int
int
2
'l'
signed long
int
4
'L'
unsigned long
int
4
'q'
signed long long
int
8
'Q'
unsigned long long
int
8
'f'
float
float
4
'd'
double
float
8
5. 进程锁
进程锁同线程锁使用方法一致,lock在不同进程使用同一共享内存时,能够确保进程之间互不影响。
使用lock的方法是:在每个进程执行运算修改共享内存之前执行lock.acquire()将共享内存上锁, 确保当前进程执行时,内存不会被其他进程访问;
执行运算完毕后使用lock.release()将锁打开, 保证其他的进程可以使用该共享内存。
lock.acquire()和lock.release()必须成对出现。
如何选择多线程/多进程
1. 结论
CPU密集型代码(各种循环处理、计算等等):使用多进程
IO密集型代码(文件处理、网络爬虫等):使用多线程
2. 解释
多线程和多进程的理解可以类比于公路。
假设当前公路均为单行道,并且出于安全考虑,一个车道只能同时行驶一辆汽车,一条公路只有一名驾驶员。只有一名指挥者进行集中调度,驾驶员获取到了指挥者的调度信息才会驾驶。
单线程是只有一条公路而且是单车道,只能同时行驶一辆汽车;
多线程是只有一条公路,但是是多车道,可以同时行驶多辆汽车;
多进程是有很多条公路,每条公路可能是单车道也可能是多车道,同样可以同时行驶多辆汽车。
因为GIL的存在,python中的多线程其实在同一时间只能运行一个线程,就像一名驾驶员只能同时驾驶一辆汽车。四线程类比于一条四车道的公路,但是驾驶员可以从驾驶车道A上的汽车切换至驾驶车道B上的汽车,驾驶员切换的速度够快的话,看起来就像是这条公路上的四辆汽车都在同时行驶。指挥者发布的命令只需要跨越车道就能传递给驾驶员,命令传输的时间损耗相对较小。所以对于多线程,我们希望指挥者可以比较频繁发布命令,驾驶员获取到命令后能够很快就完成然后切换到下一个车道继续执行命令,这样看起来就像是驾驶员同时驾驶四辆汽车了。所以对于IO密集型代码,推荐使用多线程。
而对于多进程来说,每条公路都有一名驾驶员,四线程类比于四条公路,则四名驾驶员可以同时驾驶四辆汽车。但指挥者发布的命令需要跨越公路才能传递给驾驶员,命令传输的时间损耗相对较大。所以对于多进程,我们希望指挥者发布一次命令后驾驶员可以执行较长时间,这样就不必把时间过多花费在信息传输上。所以对于CPU密集型代码,推荐使用多进程。
参考资料