秦子實



摘要:編程語言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線程的執行效率。
【通聯編輯:梁書】