前段时间自己在跑代码的时候,一直被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
    5
    import 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
    11
    def 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的时候运行。