Python 并发编程之使用多线程和多处理器
在Python編碼中我們經(jīng)常討論的一個方面就是如何優(yōu)化模擬執(zhí)行的性能。盡管在考慮量化代碼時NumPy、SciPy和pandas在這方面已然非常有用,但在構(gòu)建事件驅(qū)動系統(tǒng)時我們無法有效地使用這些工具。有沒有可以加速我們代碼的其他辦法?答案是肯定的,但需要留意!
在這篇文章中,我們看一種不同的模型-并發(fā),我們可以將它引入我們Python程序中。這種模型在模擬中工作地特別好,它不需要共享狀態(tài)。Monte Carlo模擬器可以用來做期權(quán)定價以及檢驗算法交易等類型的各種參數(shù)的模擬。
我們將特別考慮Threading庫和Multiprocessing庫。
Python并發(fā)
當(dāng)Python初學(xué)者探索多線程的代碼為了計算密集型優(yōu)化時,問得最多的問題之一是:”當(dāng)我用多線程的時候,為什么我的程序變慢了?“
在多核機器上,我們期望多線程的代碼使用額外的核,從而提高整體性能。不幸的是,主Python解釋器(CPython)的內(nèi)部并不是真正的多線程,是通過一個全局解釋鎖(GIL)來進(jìn)行處理的。
GIL是必須的,因為Python解釋器是非線程安全的。這意味著當(dāng)從線程內(nèi)嘗試安全的訪問Python對象的時候?qū)⒂幸粋€全局的強制鎖。在任何時候,僅僅一個單一的線程能夠獲取Python對象或者C API。每100個字節(jié)的Python指令解釋器將重新獲取鎖,這(潛在的)阻塞了I/0操作。因為鎖,CPU密集型的代碼使用線程庫時,不會獲得性能的提高,但是當(dāng)它使用多處理庫時,性能可以獲得提高。
并行庫的實現(xiàn)
現(xiàn)在,我們將使用上面所提到的兩個庫來實現(xiàn)對一個“小”問題進(jìn)行并發(fā)優(yōu)化。
線程庫
上面我們提到: 運行CPython解釋器的Python不會支持通過多線程來實現(xiàn)多核處理。不過,Python確實有一個線程庫。那么如果我們(可能)不能使用多個核心進(jìn)行處理,那么使用這個庫能取得什么好處呢?
許多程序,尤其是那些與網(wǎng)絡(luò)通信或者數(shù)據(jù)輸入/輸出(I/O)相關(guān)的程序,都經(jīng)常受到網(wǎng)絡(luò)性能或者輸入/輸出(I/O)性能的限制。這樣Python解釋器就會等待哪些從諸如網(wǎng)絡(luò)地址或者硬盤等“遠(yuǎn)端”數(shù)據(jù)源讀寫數(shù)據(jù)的函數(shù)調(diào)用返回。因此這樣的數(shù)據(jù)訪問比從本地內(nèi)存或者CPU緩沖區(qū)讀取數(shù)據(jù)要慢的多。
因此,如果許多數(shù)據(jù)源都是通過這種方式訪問的,那么就有一種方式對這種數(shù)據(jù)訪問進(jìn)行性能提高,那就是對每個需要訪問的數(shù)據(jù)項都產(chǎn)生一個線程 。
舉個例子,假設(shè)有一段Python代碼,它用來對許多站點的URL進(jìn)行扒取。再假定下載每個URL所需時間遠(yuǎn)遠(yuǎn)超過計算機CPU對它的處理時間,那么僅使用一個線程來實現(xiàn)就會大大地受到輸入/輸出(I/O)性能限制。
通過給每個下載資源生成一個新的線程,這段代碼就會并行地對多個數(shù)據(jù)源進(jìn)行下載,在所有下載都結(jié)束的時候再對結(jié)果進(jìn)行組合。這就意味著每個后續(xù)下載都不會等待前一個網(wǎng)頁下載完成。此時,這段代碼就受收到客戶/服務(wù)端帶寬的限制。
不過,許多與財務(wù)相關(guān)的應(yīng)用都受到CPU性能的限制,這是因為這樣的應(yīng)用都是高度集中式的對數(shù)字進(jìn)行處理。這樣的應(yīng)用都會進(jìn)行大型線性代數(shù)計算或者數(shù)值的隨機統(tǒng)計,比如進(jìn)行蒙地卡羅模擬統(tǒng)計。所以只要對這樣的應(yīng)用使用Python和全局解釋鎖(GIL),此時使用Python線程庫就不會有任何性能的提高。
Python實現(xiàn)
下面這段依次添加數(shù)字到列表的“玩具”代碼,舉例說明了多線程的實現(xiàn)。每個線程創(chuàng)建一個新的列表并隨機添加一些數(shù)字到列表中。這個已選的“玩具”例子對CPU的消耗非常高。
下面的代碼概述了線程庫的接口,但是他不會比我們用單線程實現(xiàn)的速度更快。當(dāng)我們對下面的代碼用多處理庫時,我們將看到它會顯著的降低總的運行時間。
讓我們檢查一下代碼是怎樣工作的。首先我們導(dǎo)入threading庫。然后我們創(chuàng)建一個帶有三個參數(shù)的函數(shù)list_append。第一個參數(shù)count定義了創(chuàng)建列表的大小。第二個參數(shù)id是“工作”(用于我們輸出debug信息到控制臺)的ID。第三個參數(shù)out_list是追加隨機數(shù)的列表。
__main__函數(shù)創(chuàng)建了一個107的size,并用兩個threads執(zhí)行工作。然后創(chuàng)建了一個jobs列表,用于存儲分離的線程。threading.Thread對象將list_append函數(shù)作為參數(shù),并將它附加到j(luò)obs列表。
最后,jobs分別開始并分別“joined”。join()方法阻塞了調(diào)用的線程(例如主Python解釋器線程)直到線程終止。在打印完整的信息到控制臺之前,確認(rèn)所有的線程執(zhí)行完成。
我們能在控制臺中調(diào)用如下的命令time這段代碼
將產(chǎn)生如下的輸出
注意user時間和sys時間相加大致等于real時間。這表明我們使用線程庫沒有獲得性能的提升。我們期待real時間顯著的降低。在并發(fā)編程的這些概念中分別被稱為CPU時間和掛鐘時間(wall-clock time)
多進(jìn)程處理庫
?
為了充分地使用所有現(xiàn)代處理器所能提供的多個核心 ,我們就要使用多進(jìn)程處理庫 。它的工作方式與線程庫完全不同 ,不過兩種庫的語法卻非常相似 。
多進(jìn)程處理庫事實上對每個并行任務(wù)都會生成多個操作系統(tǒng)進(jìn)程。通過給每個進(jìn)程賦予單獨的Python解釋器和單獨的全局解釋鎖(GIL)十分巧妙地規(guī)避了一個全局解釋鎖所帶來的問題。而且每個進(jìn)程還可獨自占有一個處理器核心,在所有進(jìn)程處理都結(jié)束的時候再對結(jié)果進(jìn)行重組。
不過也存在一些缺陷。生成許多進(jìn)程就會帶來很多I/O管理問題,這是因為多個處理器對數(shù)據(jù)的處理會引起數(shù)據(jù)混亂 。這就會導(dǎo)致整個運行時間增多 。不過,假設(shè)把數(shù)據(jù)限制在每個進(jìn)程內(nèi)部 ,那么就可能大大的提高性能 。當(dāng)然,再怎么提高也不會超過阿姆達(dá)爾法則所規(guī)定的極限值。
Python實現(xiàn)
使用Multiprocessing實現(xiàn)僅僅需要修改導(dǎo)入行和multiprocessing.Process行。這里單獨的向目標(biāo)函數(shù)傳參數(shù)。除了這些,代碼幾乎和使用Threading實現(xiàn)的一樣:
控制臺測試運行時間:
得到如下輸出:
在這個例子中可以看到user和sys時間基本相同,而real下降了近兩倍。之所以會這樣是因為我們使用了兩個進(jìn)程。擴展到四個進(jìn)程或者將列表的長度減半結(jié)果如下(假設(shè)你的電腦至少是四核的):
使用四個進(jìn)程差不多提高了3.8倍速度。但是,在將這個規(guī)律推廣到更大范圍,更復(fù)雜的程序上時要小心。數(shù)據(jù)轉(zhuǎn)換,硬件cacha層次以及其他一些問題會減弱加快的速度。
在下一篇文章中我們會將Event-Driben Basketer并行化,從而提高其運行多維參數(shù)尋優(yōu)的能力。
總結(jié)
以上是生活随笔為你收集整理的Python 并发编程之使用多线程和多处理器的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: python 动态规划例子
- 下一篇: Python串行运算、并行运算、多线程、