Python函数并行执行方法的研究与实践
2021-03-15秦子实
秦子实
摘要:编程语言Python因其简洁的语法、强大的表达能力、方便的基础库以及丰富的第三方库,被广泛应用于各个行业的日常业务中。近年来,随着Python大版本的更新,引入了越来越多的并行执行基础库,以方便编程人员优化各种场景下的代码执行效率。本文介绍了Python在近几个版本中新引入的并行库在常见场景下的代码执行效率优化方法,方法具有代码改动小、优化效果显著、方便部署等特点,适合Python使用者在各场景中的程序执行效率优化。
关键词:多进程;异步函数;Python
中图分类号:TP393 文献标识码:A
文章编号:1009-3044(2021)03-0050-02
1 概述
随着Python语言在各行各业的广泛使用,代码规模和执行任务的数量都显著增长,因此,部分代码的执行效率优化越来越重要。同其他流行编程语言类似,新版的Python同样引入了越来越多并行执行基础库,例如3.3版本以来大量引入的多进程库multiprocessing,以及3.6版本以来大量引入的协程库asyncio等。Python的诸多应用场景,均存在使用多核CPU执行多任务程序的情形,例如通过网络下载的线程任务,或是利用win32的API操作COM的进程任务。若使用单进程同步阻塞运行,则不能充分利用硬件的算力和I/O带宽,大量的CPU周期浪费在等待响应中,导致执行时间成倍增长。
本文针对上述需求,根据Python并发库的特点,按照不同场景研究并实践了一套任务并行开发方法。该方法利用Python基础库,可以在现有代码上实现大幅提升阻塞式代码的执行效率。
2 进程并行
2.1 应用场景
在特定场景下,代码必须在进程环境下运行,比较常见的是在使用pywin32的API操作COM组件的场景下,例如使用Python操作Office或建立WMI连接,此类场景中,必须为COM组件建立单独的进程。本文以WMI连接为例,介绍多进程并发建立WMI连接。
2.2 实现方案
并發多个WMI连接时,每个连接均需要使用单独的进程发起。对于此类并发多个相同或相似进程的场景,通常使用multiprocessing.pool.Pool类创建进程池实例,进而使用apply、map系列函数。
需要注意的是,在Windows系统上,当使用multiprocessing的程序有冻结并生成Windows可执行程序的需求时(例如使用py2exe、PyInstaller、cx_Freeze等打包程序),需要在使用多进程模块前调用freeze_support函数。
在使用进程池建立WMI连接时,使用Pool创建进程池,并使用参数processes指定进程数,通常进程数小于逻辑核心数。
之后使用pool.imap_unordered生成一个进程池的可迭代对象,将WMI连接函数read_wmi_func传入,并指定一个IP列表ip_list作为进程池中所有进程的参数。该可迭代对象用于遍历并得到进程池中每个进程的返回值。
该函数的行为可概括为:创建若干个worker进程(数目由processes参数指定),每个worker初始化环境并运行read_wmi_func函数,并从ip_list中依次取出每个成员作为参数传给各worker中的函数。
需要了解的是,“imap_unordered”为apply、map系列函数中的一个,函数名中的“i”即iterable,说明该函数返回可迭代对象,是一个惰性求值函数;“map”指分发任务的方式为map映射;“unordered”说明该函数在使用参数集合ip_list创建新进程时,不会依次等待前一个进程结束,即该函数的worker在完成当前任务后立刻取下一个参数并执行,而不是等待前面的worker执行完成再依次取参数。apply、map系列函数的命名均遵循此类规则。在示例的WMI场景中,任务直接并无先后次序,也无须相互等待依次完成,因此,使用“imap_unordered”的效率较高。
3 协程异步
3.1 技术背景
Python的协程解决方案是在3.6版本后逐步引入的,总体而言,协程是基于同一个线程的,即协程是单线程的。与以往的多线程解决方案不同的是,多线程通常是多个线程运行多个任务,每个任务都是阻塞式,即该线程中的任务执行完成后,才能继续执行下一个进程。协程在一个线程中维护一个事件循环,协程中执行的事件均为非阻塞的,即一个事件执行后立即接着执行下一个事件,事件循环会轮询检查各事件是否执行完成,完成的获得返回值或执行回调函数。
多线程方案通常存在资源锁及抢占问题,在多个线程同时访问资源产生;而协程为同一个线程,在不同的时间进行访问,只需要梳理清楚执行流程,即可以同步方式编写异步程序。
3.2 应用场景
以非阻塞方式运行一个Python函数,一般有两个应用场景:将一个普通Python函数放入事件循环执行,或是直接执行一个异步函数。
目前,仍有大量Python库尚不包含异步API,使用asyncio对普通函数进行封装,并异步执行普通Python函数有较多的应用场景。而有部分常见库的最新版本已经支持了异步API,支持直接通过asyncio.run()异步执行。
3.3 异步执行普通函数
使用asyncio执行普通函数,需要先将普通函数封装为future对象,然后将这些future对象传给asyncio.gather统一执行,并返回结果。http下载就是较为常见的场景,例如需要获取某链接列表中的所有url内容,假设列表中有5个链接:同步模式下,即在循环中使用requests.get依次下载列表中的所有url内容并将其结果放入结果列表中,如此,需要的总时间大约为内容传输以及网络延迟时间的5倍;异步模式下,asyncio将一次性发送5个GET请求并等待远端返回,在远端全部返回结果后结束并一次性返回所有结果,需要的总时间大约为所有GET请求中最慢的一个请求时间。
上述示例的返回即为有5个“
3.4 异步执行协程
同样使用http下载的例子,httpx是一个与requests库类似且具有异步API的常用网络工具库。
需要注意的是,在循环中创建协程,应当使用一个循环创建,再使用另一个循环等待结果,而不是在同一个循环中创建一个协程就等待一次结果,如此编写则为使用同步方式运行协程,失去了异步方式提升I/O效率的功能。
4 结束语
本文介绍了在诸如I/O等存在较长阻塞时间的场景中,提升代码执行效率的两种常见方式,在Python中恰当地使用多进程或协程可以成倍提升代码执行效率。特别的,Python默认C语言实现存在线程的全局解释器锁(global interpreter lock),因此正确的利用协程解决方案可以极大地提升Python线程的执行效率。
【通联编辑:梁书】