新闻动态

完善的7*24小时服务,携手共赢,共同成长

首页 新闻动态公司动态 正文

SpeedyCloud汪尊:浅析Python多线程和多进程的使用

2015-09-23 13:25:41   
9月19日,2015PyCon大会北京站成功举办。SpeedyCloud攻城狮汪尊为现场的Pythoneer分享了Python多线程与多进程的使用。
9月19日,2015PyCon大会北京站成功举办。SpeedyCloud攻城狮汪尊为现场的Pythoneer分享了Python多线程与多进程的使用,以下是演讲内容:

 

今天我演讲的题目是浅析Python多线程与多进程的使用。之前我们刚完成一监控项目,我们是做云计算的,监控对象是云主机、云硬盘、云数据库、云分发等等,如它们的内存、带宽、IOPS、QPS等各项指标,看是否存在异常。因为监控对象多,我们选择多线程来完成任务。

使用多线程,最主要的目的就是为了提高执行效率,充分利用系统资源。接下来说说Python多线程与多进程的使用。

项目最原始的版本里,我使用多线程来实现,但在团队进行Code Review时,架构师同学建议改用多进程来完成该任务,如下图:

当时我就奇怪为什么放弃多线程而使用多进程呢?毕竟创建一个进程的开销比创建线程的开销大,而且进程通信相当麻烦。

后来架构师让我研究了一下GIL,研究GIL后发现——上当了!之前学过JAVA,一直以为Python和JAVA的多线程没有区别,但事实上Python多了GIL。

什么是GIL?

GIL(Global Interpreter Lock 全局解释器锁)官方解释是计算机程序设计语言解释器用于同步线程的工具,使得任何时刻仅有一个线程在执行。简单来说,GIL是一把超级大锁,以阻止原生线程并发执行Python字节码,因为内存线程不安全。

不过GIL也顺带把Python多线程给干掉了:GIL会把多线程序列化,来阻止线程并发。

多线程测试

现在我们通过一个例子来了解GIL的功能:

定义一个无限死循环的函数。我的CPU是4核,于是创建4个线程,这4个线程启动后,理论上CPU使用率应达到400%左右,然而当我们看到执行结果时,却发现CPU使用率只达到150%左右,如下图:

为什么会出现这种情况呢?

原因在于GIL阻止了线程并发。

既然GIL会阻止线程并发,Python为什么还有多线程机制呢?python多线程是否毫无用处? 当然不是!

计算机内的任务分为两种:

  1. I/O密集型
  2. CPU密集型

I/O密集型任务

在I/O密集型操作上,多线程的优势比较明显,举个例子:

从网站爬图,爬图是典型的I/O密集型,因为需要不断发请求,不断读图片和写图片。

如图第一个爬图用单线程实现,用了2分16秒,而改用多线程后只用了18秒,可见多线程处理I/O密集型操作,执行效率比单线程高得多。

为什么呢?我们可以看看I/O密集型执行流程图:

  1. 第1个线程开始执行,执行到I/O操作时,GIL将被释放,线程1等待I/O响应
  2. 线程2获得GIL,开始执行,执行到I/O操作时,也释放GIL,等待I/O响应
  3. 线程3获得GIL,开始执行,执行到I/O操作时,也释放GIL等待I/O响应
  4. 此时线程1完成I/O操作,它将重新获得GIL,接着往后执行
  5. 执行程序剩余步骤

小结:

在 I/O 密集型任务中,由于等待I/O响应的时间比CPU计算时间长得多,所以在I/O密集型操作中,多个线程之间有效利用了I/O等待的时间,从而达到了并行的效果。

CPU密集型任务

CPU密集型操作时,Python多线程功能是否有效呢?

因为CPU的运行速度比I/O要快得多,没有I/O密集型任务的时间间隙来实现伪并发,所以,在CPU密集型操作中,多线程毫无优势。举例:

创建800个数组,每个数组包含800个数字,然后对800数组进行排序,排序需要不停的比较大小,是典型的CPU密集型任务

我们可以看到单线程执行用了20秒,而多线程却用了22秒多,这时候多线程比单线程反而要慢

小结:

在 CPU 密集型任务中,并不能实现真正的多线程操作

如何解决CPU密集型多线程效率低下的问题呢?

第一招:Ctypes

Ctypes提供了方便的接口,可通过其调用C语言的动态库。

我们用C写了一个无限循环的小函数,编译生成动态库后,在Python里通过Ctypes调用这个动态库,然后创建4个进程。 Ctypes调用C前会释放GIL,所以Ctypes调用的C代码不受GIL的限制,并发性高。

从图上可知,CPU4个核心基本上全跑满了,CPU总体使用率达到375%左右,因此用Ctypes成功地绕过了GIL。

但并非所有Pythoneer都精通C语言,那不会C语言的我们就束手无策了吗? 当然不是!

第二招:Multiprocessing

Python知道它的多线程存在问题,于是提供了另一个库Multiprocessing——多进程库,它允许我们以多进程的方式突破多线程的限制。

还是回到800个数组的例子,前面我们已经知道多线程和单线程都用了20多秒。但改用多进程,发现仅用了9秒多,效率大幅度提升。

Multiprocessing还有一个鲜为人知的库即dummy库,这个库和processing几乎一模一样,不同的是processing库作用于进程,而dummy库作用于线程。

多线程和多进程使用中的坑

分享了Python多线程和多进程的使用后,来说说使用它们时遇到的坑,让大家避免类似的错误。

第一个坑:join()

调用Join()的正确方式是两个for:

第一个for创建并开始线程 第二个for join()线程

很多人尤其是新手,看到这种方式往往本能地把它改成右侧的方式,如下图:

如果1个for就能完成,为什么使用2个for呢?

其实是不了解join()的功能导致的:当一个线程调用了join()后,那这个线程的主线程会被阻塞,直到这个线程执行完毕,才会接着执行主线程。

上面右图中,当down开始后立刻执行join,那for循环会被阻塞,直到down执行完再执行下一个,相当于从头到尾是串行操作。

第二个坑:File Discripter(文件描述符)

在linux内,对所有设备或文件的操作都是通过文件描述符来进行的,而Python多进程在linux平台上的实现方式是在父进程里直接fork()出各个子进程,其子进程将继承父进程的文件描述符,包括位置文件、文件偏移量、设备状态等。

当子进程去访问如日志文件,将在父进程的基础上操作,这是不合理的:

正确的方式是当你生成子进程后,立即对其初始化。重新初始化数据库链接,重新打开日志文件,这样才能保证程序不会出现莫名奇妙的错误。

以上就是我的演讲内容,谢谢大家!