python编写代码避免内存增加_读书笔记(4): 编写高质量python代码的59个有效方法...

前言

《编写高质量python代码的59个有效方法》这本书分类逐条地介绍了编写python代码的有效思路和方法,对理解python和提高编程效率有一定的帮助。本笔记简要整理其中的重要方法。

本篇介绍关于并发及并行

5. 并发及并行

并发(Concurrency):操作系统在各程序之间迅速切换,使其都有机会运行在一个CPU上。(并非真正意义上的,同一时间做很多不同的任务)

并行(Parallelism): 多核计算机可以同一时间做很多不同的任务。

并发与并行的关键区别在于,能不能提速,并行是可以做到提速的。Python写并发是比较基础的,而做到真正的并行是比较复杂的。

subprocess模块管理子进程

python内置的subprocess模块可以有效的运行并管理进程,使得Python语言能够很好地将命令行实用程序等工具结合起来。

由Python启动的多个子进程是可以平行运作的:

import subprocess

proc=subprocess.Popen(['echo','Hello from the child!'],

stdout=subprocess.PIPE)

out,err=proc.communicate()

print(out.decode('utf-8')) ##Hello from the child!

Python解释器通过Popen构造器启动echo这一子进程,通过communicate方法读取子进程的输出信息,并等待其中止。 子进程独立于父进程运行,可以定期查询子进程 状态,并处理其他事务,如下图所示:

proc2=subprocess.Popen(['sleep','0.1'],

stdout=subprocess.PIPE)

while proc2.poll() is None:

print('woring...')

print('Exit status:',proc2.poll())

60ad9066d4b6当担心子进程一直不终止,或者发生阻塞,可以给communicate方法设置timeout参数,指定子进程的响应时间:

60ad9066d4b6在Python3.3及后续版本中有效

代码其中涉及到了管道PIPE等重要的进程概念,PIPE是进程进行通信的重要方式;Communicate方法也是从PIPE中取标准的输出信息等,

Subprocess.Pipe

Communicate

可以用线程进行阻塞式I/O,不能做平行计算

标准的Python实现称为CPython,CPython分两步运行Python程序:1.把源代码解析编译乘字节码,.pyc; 2. 基于栈的解释器运行字节码。

执行Python程序式,字节码解释器必须保持协调一直的状态,Python采用GIL(全局解释器锁机制)来保证协同。

GIL本质上是把互斥锁,以防止Cpython收到抢先式多线程切换,这种切换可能破坏解释器状态。GIL可保证每条字节码指令能够正确地与Cpython实现及其C语言扩展模块协同运作。

GIL的最严重的负面影响是:Python在同一时刻只有一条线程可以执行,也就是说Python不能像C++/Java等一样使用多线程编程。

以原始的因数分解为例,单线程的写法如下:运行耗时1.87s

import time

def factorize(number):

for i in range(1,number+1):

if number%i==0:

yield i

start=time.time()

for number in [2139079,12144759,151232,12324232]:

list(factorize(number))

end=time.time()

print('Took %.3f seconds'%(end-start))

同时还采用Python多线程Threading来进行计算,其耗费时间竟然达到2.412s.(同一机器运行)。理论上,在扣去进程创建等开销后,程序运行速度应该是原来的4倍,然而事实上多线程的程序执行比单线程还慢。这说明Python的多线程是比较有局限的。

class MyThread(Thread):

def __init__(self,number):

super().__init__()

self.number=number

def run(self):

self.factors=list(factorize(self.number))

start=time.time()

threads=[]

for number in [2139079,12144759,151232,12324232]:

thread=MyThread(number)

thread.start()

threads.append(thread)

for thread in threads:

thread.join()

end=time.time()

print('Took %.3f seconds'%(end-start))

而为何这种情况下,Python还要提供Threading等多线程库呢?主要有两个原因:

多线程使得程序看起来能够在同一时间做多个任务,如果要自己实现这种效果,并手工管理任务之间的切换,比较困难

处理阻塞式I/O操作,可以借助线程将Python与耗时的I/O操作隔离开。执行系统调用时,可能会触发此类操作,如读写文件、网络间通信以及与显示器等设备进行交互等,这些都属于阻塞式的I/O操作

以下面的例子进行分析,执行系统该调用select,该函数请求系统阻塞0.1s,然后把控制器换给程序。

60ad9066d4b6

这样的写法使得主程序阻塞在select系统调用种,这时需要考虑把系统调用放到其他线程中:

如下所示:将系统调用放到多条线程中执行,使得程序既能够与多个串口通信,也能执行在主线程中所需的计算。

60ad9066d4b6尽管受制于GIL,当多个Python线程执行系统调用时,这些系统调用可以平行执行

在线程中使用Lock来防止数据竞争

尽管Python受制于GIL,无法进行真正的多线程,但在线程程序编写中还是会存在资源争夺的问题,需要进行一定的设计,保证程序的正确运行。同一时间虽然只有一个Python线程在运行,但是当这个线程操作数据时,其他线程可能会打断它。这种中断现象随时可能发生,会破坏程序的状态,影响数据的一致性。

Python在threading中提供了锁工具,保护数据不受破坏,Lock类,该类相当于互斥锁。用锁来包含可能被抢占的资源/数据,同一时刻只能有一个线程获得该锁。

60ad9066d4b6

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

Python中常用Pipeline来协调多个事务,Pipeline的工作原理与组装生产线相似,分为多个首尾相连的阶段(Phase),每个阶段由一个具体的函数负责。程序总是把待处理的新部件添加到管线的开端。每一种函数都可以在其所负责的那个阶段内,并发地处理位于该阶段的部件。涉及阻塞式I/O操作或子进程的工作任务,适合用此办法处理。

可以通过自编队列来实现管线,然而往往比较困难。推荐使用Queue类来弥补自编队列的缺陷。

from queue import Queue

queue=Queue()

def consumer():

print('Consumer waiting')

queue.get() #取队列中的任务

print('Consumer done')

thread=Thread(target=consumer)

thread.start() # 启动线程

print('Producer putting')

queue.put(object()) # 任务放入Queue任务队列

thread.join() # 等待任务线程结束

print('Producer done')

此外,可以限定队列中待处理的最大任务数量,使得相邻的两个阶段,可以通过该队列平滑地衔接起来。

60ad9066d4b6

这个地方的代码和例子比较复杂,感兴趣的请仔细看原书。

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

线程存在三个显著的缺点:

为了数据安全,必须使用特殊的工具协调线程,比较复杂;

线程需要占用大量内存,每个正在执行的线程,约占8MB内存。当线程量较大时会给计算机带来较大压力

线程启动时开销比较大 如果程序不停创建新线程来同时执行多个函数,并等待这些线程结束,那么使用线程所引发的开销,会拖慢整个程序速度。

Python的协程(coroutine)概念可以避免整个问题,使得程序看上去像是在同时运行多个函数。协程的实现方式,实际上是对生成器的一种扩展。开销比较低(与函数调用相近),占用内存较低。

def my_coroutine():

while True:

recv=yield # 接受回传值

print('Received:',recv)

it = my_coroutine()

next(it) # 初次执行生成器,让生成器进入到第一条yield表达式中

it.send('First')

it.send('Second')

# output:

#Received: First

#Received: Second

如上例所示:工作原理如下:当生成器函数执行到yield表达式时,执行相应的代码,通过send方法给生成器回传一个值。生成器收到该值后,会将其视为yield表达式的执行结果。 如上面代码注释所示,在调用send方法前,需要先调用一次next函数,以便将生成器推进到第一条yield表达式,之后就能将生成器(yield)和send操作结合起来,使得生成器能够根据外界所输入的数据,用一套标准流程产生对应的输出值。

再看下面这个更有趣的例子,这个生成器协程可以统计目前输入的最小值。

def minimize():

current=yield ## 执行第一次send时触发,将第一个输入的值当作目前的最小值,以便于后续对比

print('curr')

while True:

print('value')

value = yield current

current=min(value,current)

it = minimize()

next(it)

it.send(10)

it.send(4)

it.send(22)

it.send(2)

#output:

#curr

#value

#value

#value

#value

# 2

协程也是独立的函数,可以消耗由外部环境所传入的输入数据,并产生相应的输出结果。与线程不同的是:协程会在每个yield表达式暂停,等到外界再次调用send方法之后,才会继续执行到下一个yield

考虑使用concurrent.futures 实现真正的平行计算

当我们需要提升程序执行效率,节省执行时间时,可以考虑如何实现真正的平行计算,可以通过内置的concurrent.futures模块,来利用multiprocessing内置模块。这种做法会以子进程的形式,平行运行多个解释器,使得程序能够利用多核CPU,由于子进程与主解释器相分离,所以其GIL相互独立,每个子进程都可以完整利用一个内核。子进程同时与主进程存在联系,通过这条联系渠道,子进程可以接收主进程发过来的指令,并把计算结果返回给主进程。

60ad9066d4b6

60ad9066d4b6

这两个例子都是通过concurrent,futures来利用Multiprocessing模块,作者认为需要尽量避开multiprocessing里复杂的特性,初级的情况下优先用concurrent.futures来调用multiprocessing中线程或者进程来简单加速,更复杂的情况再使用multiprocessing.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值