前段时间自己在跑代码的时候,一直被IO读的太慢的问题所烦恼。于是乎,就顺便了解了下python多进/线程的实现和原理。
前言
一般计算机的程序有两种,CPU bound(或者说是CPU itensive)和IO bound。什么意思呢,如果一个程序提高CPU运算速度,就可以实现程序的提速,那这个程序就是CPU bound的,也就是CPU是这个程序的瓶颈。但如果另一个程序,你即使将CPU运算速度提高了很多倍,程序的运行时间变化不大,那这个程序很可能就是IO bound的,也就是说IO是这个程序的瓶颈。
为了更有效的实现数据的读取,我打算使用python的多进/线程,然后就有了这篇博文。
为什么说python的多线程是假的
python的多线程可以通过下面的这个库调用
1 | import thread |
python的多线程的确是假的,这个假体现在,多线程的运行并不是并行的。而是串行的。(so sad ;´༎ຶД༎ຶ`) 。这样说,岂不是很弱智吗?为什么当时python的设计者非得这样做呢?原因有二:
python产生的有点早
python产生的那个时间,计算机还很落后,计算机cpu都是单核的,根本就没有多线程的用武之地
python的存储管理(memory management)机制
我在这里说的存储管理机制主要指Python的垃圾回收(Garbage Collection)机制。在学习C语音的时候,我们都知道在allocate memory之后,一旦这个变量不用的,memory一定要free掉,不然会造成存储泄漏,就没有内存用了。
当然,像python这种高级语言,自然不用programmer自己去做这件事,python可以帮你搞定,python的存储管理机制共有两种:
- Reference counting
- Garbage collection
Reference counting
所谓的引用计数是这样的,python会对object被其他object引用的次数进行计数。计数可以通过以下程序获得:
1
2
3
4
5import sys
a = []
b = a
sys.getrefcount(a)
3上面的程序对于空列表的引用次数一共是3次,分别是a,b,和函数的参数。
而当一个object的引用次数变为0的时候,这个object所占的内存便会被释放:
1
2
3
4
5
6# Literal 9 is an object
b = 9
# Reference count of object 9
# becomes 0.
b = 4如上面的代码,第一行赋值之后,9的引用次数变为了1,第二行之后,9的引用次数变味了0,所占的内存便会被释放掉。
Garbage collection
既然有了Reference counting这种机制,为什么还要Garbage collection呢?因为有一种情况,会使得object的引用无法为零,这种方式就是自己引用自己。
1
2
3
4
5
6
7
8
9
10
11def create_cycle():
# create a list x
x = [ ]
# A reference cycle is created
# here as x contains reference to
# to self.
x.append(x)
create_cycle()上面的代码,列表x引用了自己,这样的话,就形成了这种引用循环,Reference counting无法处理这种情况。
这时候就必须使用Garbage collection来进行内存的释放了,但这种引用循环的查找是很费计算时间的,所以Garbage collection的执行是周期性的。一般是通过一个thresholds来决定要不要执行Garbage collection。这个thresholds的定义是python在一段时间里分配内存的object个数与释放object个数之差。一旦超过这个阈值,Garbage collection就会被自动执行。
当然我们也可以手动执行Garbage collection,执行的代码如下:
1
2
3
4
5
6
7# Importing gc module
import gc
# Returns the number of
# objects it has collected
# and deallocated
collected = gc.collect()但需要注意的是,Garbage collection只会处理引用循环,它并不会处理其他引用计数不为0的object。
So? 多线程为什么需要是假的?
因为多线程是共享内存的,也就是说多个线程会共同一起修改Reference counting,这就会造成计数的值不对了,解决的办法也很简单,那就对每个计数加lock就好了。但是呢,lock加多了又会出现死锁的问题。所以python的最终的解决方案就是只留下一个lock。这个就是GIL(global interpreter lock)。也就是给解释器加lock,对于python的code,同一个时间只能有一个线程执行,所以多线程就变成了假的。也就是说,即使计算式是有多核的,这几个核也只可能交替执行,而无法在同一个时间执行。
那python该怎样并行呢?
python的并行可以用多进程来实现:
1 | from multiprocessing import Process |
多进程之间的内存是不共享的,每个进程拥有自己的GIL,所以不会出现多线程的问题。
一些介绍python mutiprocessing的blog:
python的多线程是鸡肋吗?
答案可以说不是。因为多线程还是有用武之地的。当一个python程序是IO bound的时候,多线程是可以用来解决这个问题的。
当python调用C的链接库的时候,GIL会在这之前被释放。I/O操作会调用操作系统内建的c代码,GIL会在I/O被调用之前被释放,以允许其他线程在这个线程等待I/O的时候运行。