python多线程、多进程 初探
原先刚学Java的时候,多线程也学了几天,后来一直没用到。然后接触python的多线程的时候,貌似看到一句”python多线程很鸡肋“,于是乎直接跳过了多线程的学习。
接触爬虫,才开始用到多进程这个东西。
既然用到了,就系统地学吧。先来python的,再总结一下Java的。
什么是线程和进程
很经典的一个解释是“进程是资源分配的最小单位,线程是CPU调度的最小单位“。
比如我们在任务管理器中看到的就是进程,例如qq.exe,qq就是一个进程。打开qq后,启动多条线程,各自负责文件下载、更新朋友圈、聊条等功能。
每个进程,都有自己的地址空间、内存、数据栈以及其他用于跟踪执行的辅助数据。进程可以通过派生(fork或spawn)新的进程来执行其他任务,不过这个新的进程是独立的,所有进程之间只能采用IPC(进程间通信)的方式共享信息。
线程,于进程类似,有时候也称为轻量级进程,可以把他们 视为一个主进程中并行运行的一些”迷你进程“。同一个进程下的线程共享资源。
以前单核CPU时代,多线程用处并不多。进程和线程没法做到真正的并行运算,而如今多核时代,真正能实现多条线程一起跑。
线程于进程的对比
对比维度
进程
线程
数据共享、同步
IPC共享数据较复杂,同步简单
同进程下线程数据共享,但同步复杂
可靠性
进程间互相独立
一个线程坏掉可能使整个进程挂掉
资源消耗
独立资源,消耗多。进程间切换慢
切换快,共享资源,资源消耗少
总结,进程和线程还可以类比为火车和车厢:
线程在进程下行进(单纯的车厢无法运行)
一个进程可以包含多个线程(一辆火车可以有多个车厢)
不同进程间数据很难共享(一辆火车上的乘客很难换到另外一辆火车,比如站点换乘)
同一进程下不同线程间数据很易共享(A车厢换到B车厢很容易)
进程要比线程消耗更多的计算机资源(采用多列火车相比多个车厢更耗资源)
进程间不会相互影响,一个线程挂掉将导致整个进程挂掉(一列火车不会影响到另外一列火车,但是如果一列火车上中间的一节车厢着火了,将影响到该趟火车的所有车厢)
进程可以拓展到多机,进程最多适合多核(不同火车可以开在多个轨道上,同一火车的车厢不能在行进的不同的轨道上)
进程使用的内存地址可以上锁,即一个线程使用某些共享内存时,其他线程必须等它结束,才能使用这一块内存。(比如火车上的洗手间)-”互斥锁(mutex)”
进程使用的内存地址可以限定使用量(比如火车上的餐厅,最多只允许多少人进入,如果满了需要在门口等,等有人出来了才能进去)-“信号量(semaphore)”
p ython的多线程为何鸡肋
因为GIL,全局解释锁机制,python的多线程是伪多线程。
准确来说,GIL并不是python的机制,而是CPython解释器(主流解释器)的机制(比如Jpython就没有GIL)。
什么是GIL
其实有点类似于单核CPU执行多进程的过程,虽然可以有多个进程,但只有一个CPU啊,一个进程执行的时候别的进程只能等,只是快速切换执行感觉上是一起执行罢了。现在多核CPU能做到真正的并行
在 Cpython 解释器(Python语言的主流解释器)中,有一把全局解释锁(Global Interpreter Lock),在解释器解释执行 Python 代码时,先要得到这把锁。锁只有一把,一个线程执行的时候,别的线程也得等,多核CPU也没用。(真正的多线程,比如Java的多线程,在多核情况下,可以实现真正的多条线程同步运行)
1.设置GIL
2.切换到一个线程去执行
3.运行
指定数量的字节码指令
线程主动让出控制(可以调用time.sleep(0))
4.把线程设置完睡眠状态
5.解锁GIL
6.再次重复以上步骤
线程什么时候会让出全局锁呢?一是执行满 100 tick(可以理解为100单位的指令),二是遇见了阻塞(比如I/O)。
考虑两种情况
计算密集:这时,几乎没一个线程都是执行满100tick后让出锁的,所以和单线程跑没啥区别,而且线程切换也需要时间,最终程序运行时间比起单线程反而更长。
I/O密集型:一个线程只执行了几tick,遇见I/O阻塞。这时候代码已经解释过了,可以独立执行I/O过程,这是切换到另一个线程,同理。最终类似于真正的多线程,在I/O上节省的时间多余在线程切换消耗的时间,比单线程执行快。
因而,python多线程适合I/O密集型程序。
CPython 为什么要这样设计
多线程有个问题,怎么解决共享数据的同步、一致性问题?因为,对于多个线程访问共享数据时,可能有两个线程同时修改一个数据情况,如果没有合适的机制保证数据的一致性,那么程序最终导致异常,所以,Python之父就搞了个全局的线程锁,不管你数据有没有同步问题,反正一刀切,上个全局锁,保证数据安全,简单粗暴。
这种解决办法放在90年代,其实是没什么问题的,毕竟,那时候的硬件配置还很简陋,单核 CPU 还是主流,多线程的应用场景也不多,大部分时候还是以单线程的方式运行,单线程不要涉及线程的上下文切换,效率反而比多线程更高(在多核环境下,不适用此规则)。所以,采用 GIL 的方式来保证数据的一致性和安全,未必不可取,至少在当时是一种成本很低的实现方式。
解决方案
因为Cpython的GIL,使python的多线程显得鸡肋。可以试着使用别的解释器,但更常见的作法还是使用多进程。(当然,多进程必然消耗资源多一点)。
接下来学习
多线程主要用的是标准库中的threading包,thread基本不用(主要是它在主线程结束后,其他线程会直接停止,有点类似于守护进程,显然不安全)。
多进程使用标准库中的multiprocessing。
接下来几天阅读官方文档中的这几节。