Effective python(六):并发与并行

一,使用subprocess模块管理子进程,并控制输入流和输出流

1,Popen构造器启动进程,communicate方法读取子进程输出信息

proc=subprocess.Popen(['echo','Hello'],stdout=subprocess.PIPE)
out,err=proc.communicate()
print(out.decode('utf-8'))

2,子进程会独立于父进程而运行,这里父进程指的是Python解释器

3,可以给communicate方法传入timeout参数,避免进程死锁或者失去响应,一段时间未响应自动抛出异常

二,使用线程来执行阻塞式I/O比asyncio模块简单

1,阻塞式I/O响应比较慢,应该利用线程将其分出去执行,避免程序卡死在某时间间隔内,在其响应间隔内,CPU可以执行一些计算的操作

三,使用Lock来防止数据竞争

1,线程中的暂停操作有可能导致数值混乱,即使是最简单的自增操作,也会拆分成三步,中间有一步赋值转换,如果在赋值转换的过程中,将线程打断,那么就有可能将其他线程的新值赋进去,然后转换回来的时候又赋了一遍旧值,导致数据丢失

2,Python内置的threading模块的Lock类,相当于互斥锁,通过互斥锁来保护数值,使得同一时刻多个线程中只能有一个访问该值

with lock():
    count+=1

四,使用Queue来协调各线程之间的工作

1,函数管线,每一个阶段处理好任务后将返回值传输到下一个阶段,涉及阻塞式I/O的任务很适合,这样的任务很容易分配到多个线程或进程中去

2,举例:例如照片处理系统,先从相机中获取照片(一),再调整其尺寸进行处理(二),最后上传到网络相册中(三),分别把三个阶段的函数写好,然后通过拼接为一条并发处理的管线,这种方式可以通过生产-消费队列建模

3,自己实现管线有很多缺陷,例如每个阶段执行速度有差别,某个阶段处理过慢,导致某个阶段一直没有任务可以处理,还可能某个阶段处理过快,导致队列容量不断增大,耗尽内存,进而崩溃

4,使用Queue类来弥补自己编写队列的缺陷
queue=Queue(1) #参数表示队列中最大任务数量
然后使用queue.get()queue.put()来放入和获取任务

5,还可以通过Queue类中的task_done方法来追踪任务工作进度

五,考虑用协程来并发运行多个函数

1,线程的缺点:

  • 复杂的多线程代码会令项目变得难以维护
  • 线程开销比较大,会拖慢程序执行速度本身
  • 线程需要占用大量内存,每个执行的线程大约8MB,十几个线程可以承受,但是成千上万的函数都用线程执行就会出现问题

2,Python的协程可以避免上述问题,协程的实现实际上是对生成器的扩展,启动生成器协程所需开销与调用函数相仿,处于活跃状态的协程大约只占不到1KB内存

3,协程的工作原理:每当生成器函数执行到yield表达式时,消耗生成器的代码会通过send方法发送给生成器一个值,生成器会将其视为yield表达式的执行结果,同时yield右侧的变量,为弹出值,作为send方法的返回值

#统计最小值
def minimize():
    current=yield
    while True:
        value=yield current
        current=min(value,current)

it=minimize()
# 在调用send前,先调用next()
next(it)# 将生成器推进到第一条yield表达式位置 
print(it.send(10))
print(it.send(5))

4,协程与线程类似,是独立的函数,消耗由外部传入的数据,并产生响应的输出结果,与线程不同的是,协程会在每个yield表达式处暂停,等到外界再次调用send方法后,才会执行到下一个yield表达式

5,可以利用生成器所产生的输出值,去推进其它生成器函数,连接多个生成器函数,即可模拟出Python线程的并发行为,令程序看上去像是同时运行多个函数

6,在生成器函数中添加return后,可以使用yield from表达式来调用,最后的返回值会作为yield from的结果

六,考虑用concurrent.futures实现真正的平行计算(多核处理)

1,使用内置的concurrent.futures模块来利用另一个multiprocessing的内置模块,该做法会以子进程的形式,平行运行多个解释器,利用多核CPU提升执行速度,由于子进程与主解释器分离,所以他们的GIL全局解释锁也是相互独立的

pool=ProcessPoolExecutor(max_workers=2)# 最大工作数应与CPU核心数相同
#将函数映射到数据列表上去
results=list(pool.map(func,alist))

2,ProcessPoolExecutor类利用由multiprocessing模块所提供的底层机制,逐步完成下列操作

  1. 把alist每项数据传给map
  2. 用pickle模块对数据序列化,变成二进制形式
  3. 通过本地套接字(local socket),将序列化后的数据从主解释器进程发送到子解释器进程
  4. 子进程pickle对数据库反序列化,还原成python对象
  5. 引入包含func函数的模块
  6. 各子进程平行的对各自的输入数据,运行func函数
  7. 对运行结果进行序列化
  8. 将序列化结果通过socket复制到主进程中
  9. 主进程反序列化,还原python对象
  10. 主进程把每个子进程所求出的计算结果合并到一份列表并返回

3,multiprocessing开销较高,原因在序列化与反序列化操作,对于运行函数不需要与程序其他部分共享状态,且主进程与子进程只需要传递一小部分数据,就能完成大量运算的任务来说,这套方案比较合适。如果待执行的运算不符合上述特征,那么所产生的开销可能无法通过平行化来提升程序运算速度,这种情况下,可以求助multiprocessing的高级机制,如共享内存,跨进程锁定,队列,代理等,不过这些特性用起来非常复杂,建议少碰

4,补充,引发性能瓶颈和调用率较高且对性能要求较高的部分使用C语言扩展来编写,但是工作量较大,也可能引发BUG

posted @ 2020-04-01 12:34  石天放  阅读(313)  评论(0编辑  收藏  举报