multiprocessing模块

1 Python为什么慢

我们经常听程序员争论”XXX才是世界上最好的语言!”有一个理由常常让Python的拥趸者哑口无言“Python太慢了!”

以下是 The Benchmarks Game 面向主流编程语言设计的性能测试榜单,灰色反映的是时间效率,越短代表性能越好,棕色则是基于执行时间和内存开销的加权值。

Pthon3处于垫底水平,那Python为什么会慢呢?

  1. 开发者写的代码不够pythonic

  • 对象复制

    下面两种对象复制方法有数倍的速度差异。

  • 迭代器

    用好迭代器能加速部分场景。

    迭代器有惰性求值的特性,并非一次性将所有元素加载到内存中,而是在每次需要时才计算并返回下一个元素。

    运行下面的程序,会发现get_data_1与get_data_2所用的时间类似,但是get_data_1的三个url几乎是同时输出,而get_data_2的url是一秒一个。

    把代码修改成这样应该很容易看出来了:

    不难看到,迭代器的i=0后面的程序就没有再运行了。

    • 数据结构

      不同的数据结构容易造成较大的性能差异,比如:

      set是用hash table实现的,hash表查询速度更快

  • GIL (Global Interpret Lock)

全局解释器锁,确保在任何一个时刻,同一个Python进程中只有一个线程能够执行Python字节码。因此如果两个线程并发的话还是要做序列化,所以我们才会说Python中无法实现真正的多线程。

下面是一个例子:

loop是占用了CPU,而同一进程内只能有一个线程执行,所以会慢,这个代码只是虚有多线程的壳子,没有起到多线程的作用。

2 Python并发解决方案

当然,对于IO密集型的任务(比如文件读写),上面的多线程代码是成立的,因为计算机在等待IO的过程中不占用CPU,可以切换下一个线程。针对IO密集型的任务可以使用各类异步编程库:asyncio, aiohttp, eventlet, twisted等,可以后续文章再细讲。

针对上述CPU密集型的任务(比如计算正则表达式),可以使用多进程。

将上述代码改成多进程,就可以顺利执行:

2.1 并行与并发的区别

并发(Concurrency):看起来在同时处理任务,实际可能是使用单个处理器通过快速切换交替执行,给人同时执行的错觉。

并行(Parallelism):在多核CPU或者多设备上真正的同时执行。

2.2 multiprocessing模块

Process类

创建进程的类,由该类实例化得到的对象,表示一个子进程中的任务。

p=Process()

  • p.start() 启动进程

  • p.run() 进程启动时运行的方法,用于调用target来指定需要执行的函数;这个方法通常不应该由用户直接调用

  • p.terminate() 强制终止进程p,但不会做任何的清理操作

  • p.is_alive() 判断p是否还在运行

  • p.join([timeout]) 主线程等待p结束,确保主进程在子进程完成之后再继续执行

Lock

多进程的本质是减少并发过程中锁的使用,但有些时候为了避免资源错乱,不得不用锁。

不加锁的后果:

加锁之后:

如果一个进程已经持有了某个 Lock,在再次尝试获取这个 Lock 时,会造成自死锁

相比于普通的Lock,Rlock允许同一个进程多次获取锁:

RLock 内部维护着一个持有者标识和一个递归计数器

  1. 当一个进程第一次获取 RLock 时,RLock 会记录标识,并将计数器设为 1。

  2. 同一个进程再次获取这个 RLock 时,它会检查到持有者就是自己,于是不会阻塞,而是将计数器加 1。

  3. 每次调用 release() ,计数器会减 1。

  4. 当计数器减到 0 时,锁才会被真正释放,供其它进程获取。

Pool

手动创建进程需要一个一个start并join,略为繁琐,使用进程池可以简化这一过程。

一个经典的并发过程如下图所示:

4 条件变量与生产者消费者

上面的过程实际就是生产者与消费者的关系。

现在有两个队列,生产者队列负责添加数据,消费者队列负责取出数据。

在这个过程中,有一个固定大小的缓冲区:

  • 当缓冲区已满时,生产者不应再向其中添加数据,否则会覆盖尚未被消费的数据。

  • 当缓冲区为空时,消费者不应尝试从中取出数据,因为无数据可取。

如果有一些特殊需求需要满足:

  1. 如果消费者要求必须含有至少3个数据才开始消费,应该怎么办

  2. 同理生产者呢?

  3. 如果每个消费者只能消费特定的数据,应该怎么办?

多进程中可以通过一个共享变量信号量解决:

参考

Last updated