新聞中心
在Python編碼中我們經(jīng)常討論的一個方面就是如何優(yōu)化模擬執(zhí)行的性能。盡管在考慮量化代碼時NumPy、SciPy和pandas在這方面已然非常有用,但在構(gòu)建事件驅(qū)動系統(tǒng)時我們無法有效地使用這些工具。有沒有可以加速我們代碼的其他辦法?答案是肯定的,但需要留意!

為新民等地區(qū)用戶提供了全套網(wǎng)頁設(shè)計制作服務(wù),及新民網(wǎng)站建設(shè)行業(yè)解決方案。主營業(yè)務(wù)為成都做網(wǎng)站、網(wǎng)站建設(shè)、新民網(wǎng)站設(shè)計,以傳統(tǒng)方式定制建設(shè)網(wǎng)站,并提供域名空間備案等一條龍服務(wù),秉承以專業(yè)、用心的態(tài)度為用戶提供真誠的服務(wù)。我們深信只要達(dá)到每一位用戶的要求,就會得到認(rèn)可,從而選擇與我們長期合作。這樣,我們也可以走得更遠(yuǎn)!
在這篇文章中,我們看一種不同的模型-并發(fā),我們可以將它引入我們Python程序中。這種模型在模擬中工作地特別好,它不需要共享狀態(tài)。Monte Carlo模擬器可以用來做期權(quán)定價以及檢驗算法交易等類型的各種參數(shù)的模擬。
我們將特別考慮Threading庫和Multiprocessing庫。
Python并發(fā)
當(dāng)Python初學(xué)者探索多線程的代碼為了計算密集型優(yōu)化時,問得最多的問題之一是:”當(dāng)我用多線程的時候,為什么我的程序變慢了?“
在多核機(jī)器上,我們期望多線程的代碼使用額外的核,從而提高整體性能。不幸的是,主Python解釋器(CPython)的內(nèi)部并不是真正的多線程,是通過一個全局解釋鎖(GIL)來進(jìn)行處理的。
GIL是必須的,因為Python解釋器是非線程安全的。這意味著當(dāng)從線程內(nèi)嘗試安全的訪問Python對象的時候?qū)⒂幸粋€全局的強(qiáng)制鎖。在任何時候,僅僅一個單一的線程能夠獲取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)超過計算機(jī)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ù)值的隨機(jī)統(tǒng)計,比如進(jìn)行蒙地卡羅模擬統(tǒng)計。所以只要對這樣的應(yīng)用使用Python和全局解釋鎖(GIL),此時使用Python線程庫就不會有任何性能的提高。
Python實現(xiàn)
下面這段依次添加數(shù)字到列表的“玩具”代碼,舉例說明了多線程的實現(xiàn)。每個線程創(chuàng)建一個新的列表并隨機(jī)添加一些數(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是追加隨機(jī)數(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í)行完成。
- # thread_test.pyimport randomimport threadingdef list_append(count, id, out_list):
- """
- Creates an empty list and then appends a
- random number to the list 'count' number
- of times. A CPU-heavy operation!
- """
- for i in range(count):
- out_list.append(random.random())if __name__ == "__main__":
- size = 10000000 # Number of random numbers to add
- threads = 2 # Number of threads to create
- # Create a list of jobs and then iterate through
- # the number of threads appending each thread to
- # the job list
- jobs = []
- for i in range(0, threads):
- out_list = list()
- thread = threading.Thread(target=list_append(size, i, out_list))
- jobs.append(thread)
- # Start the threads (i.e. calculate the random number lists)
- for j in jobs:
- j.start()
- # Ensure all of the threads have finished
- for j in jobs:
- j.join()
- print "List processing complete."
我們能在控制臺中調(diào)用如下的命令time這段代碼
- time python thread_test.py
將產(chǎn)生如下的輸出
- List processing complete.
- real 0m2.003s
- user 0m1.838s
- sys 0m0.161s
注意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)的一樣:
- # multiproc_test.pyimport randomimport multiprocessingdef list_append(count, id, out_list):
- """
- Creates an empty list and then appends a
- random number to the list 'count' number
- of times. A CPU-heavy operation!
- """
- for i in range(count):
- out_list.append(random.random())if __name__ == "__main__":
- size = 10000000 # Number of random numbers to add
- procs = 2 # Number of processes to create
- # Create a list of jobs and then iterate through
- # the number of processes appending each process to
- # the job list
- jobs = []
- for i in range(0, procs):
- out_list = list()
- process = multiprocessing.Process(target=list_append,
- args=(size, i, out_list))
- jobs.append(process)
- # Start the processes (i.e. calculate the random number lists)
- for j in jobs:
- j.start()
- # Ensure all of the processes have finished
- for j in jobs:
- j.join()
- print "List processing complete."
控制臺測試運行時間:
- time python multiproc_test.py
得到如下輸出:
- List processing complete.
- real 0m1.045s
- user 0m1.824s
- sys 0m0.231s
在這個例子中可以看到user和sys時間基本相同,而real下降了近兩倍。之所以會這樣是因為我們使用了兩個進(jìn)程。擴(kuò)展到四個進(jìn)程或者將列表的長度減半結(jié)果如下(假設(shè)你的電腦至少是四核的):
- List processing complete.
- real 0m0.540s
- user 0m1.792s
- sys 0m0.269s
使用四個進(jìn)程差不多提高了3.8倍速度。但是,在將這個規(guī)律推廣到更大范圍,更復(fù)雜的程序上時要小心。數(shù)據(jù)轉(zhuǎn)換,硬件cacha層次以及其他一些問題會減弱加快的速度。
在下一篇文章中我們會將Event-Driben Basketer并行化,從而提高其運行多維參數(shù)尋優(yōu)的能力。
相關(guān)閱讀:
Cholesky Decomposition in Python and NumPy
European Vanilla Call-Put Option Pricing with Python
Jacobi Method in Python and NumPy
LU Decomposition in Python and NumPy
Options Pricing in Python
QR Decomposition with Python and NumPy
Quick-Start Python Quantitative Research Environment on Ubuntu 14.04
英文原文:Parallelising Python with Threading and Multiprocessing
譯文鏈接:http://www.oschina.net/translate/parallelising-python-with-threading-and-multiprocessing
分享題目:Python 并發(fā)編程之使用多線程和多處理器
URL標(biāo)題:http://fisionsoft.com.cn/article/cdeicos.html


咨詢
建站咨詢
