正確合理地使用並發編程,無疑會給我們的程序帶來極大的性能提升。今天我就帶大家一起來剖析一下python的並發編程。這進入並發編程之前,我們首先需要先了解一下並發和並行的區別。
首先你需要知道,並發並不是指同一時刻有多個操作同時進行。相反,某個特定的時刻,它只允許有一個操作發生,只不過線程或任務之間會互相切換,直到完成。如下圖所示:
圖中出現了線程(thread) 和任務(task) 分別對應Python中兩種並發形式--多線程(threading)和協程(asyncio)。對於多線程來說,是由操作系統來控制線程切換的。而對於 asyncio來說,主程序想要切換任務時,必須得到此任務可以被切換的通知。
對於並行來說,是指同一時刻、同時執行任務。如下圖所示:
python中的多進程(multi-processing)是Python中的並行的實現形式。
對比來看,並發通常應用於I/O操作頻繁的場景,而並行通常應用於CPU負載重的場景。
單線程與多線程性能比較
下面我們來比較一下單線程和多線程的性能區別。
我們先看一下單線程版本。
import time def process(work): time.sleep(2) print('process {}'.format(work)) def process_works(works): for work in works: process(work) def main(): works = [ 'work1', 'work2', 'work3', 'work4' ] start_time = time.time() process_works(works) end_time = time.time() print('use {} seconds'.format(end_time - start_time)) if __name__ == '__main__': main() ##輸出## process work1 process work2 process work3 process work4 use 8.016737222671509 seconds
單線程是最簡單也是最直接的。
- 先是遍歷任務列表;
- 然后對當前任務進行操作;
- 等到當前操作完成后,再對下一個任務進行同樣的操作,一直到結束。
我們可以看到總共耗時約 8s。單線程的優點是簡單明了,但是明顯效率低下,因為上述程序的絕大多數時間,都浪費在了 I/O 等待上(假設time.sleep(2)是處理IO的時間)。下面我們來看一下多線程實現的版本。
import time import concurrent.futures def process(work): time.sleep(2) print('process {} '.format(work)) def process_works(works): with concurrent.futures.ThreadPoolExecutor(max_workers=4) as executor: executor.map(process, works) def main(): works = [ 'work1', 'work2', 'work3', 'work4' ] start_time = time.time() process_works(works) end_time = time.time() print('use {} seconds'.format(end_time - start_time)) if __name__ == '__main__': main() ####輸出#### process work1 process work2 process work3 process work4 use 2.006268262863159 seconds
可以看到耗時用了2s多,一下子效率提升了4倍。我們來分析一下下面這段代碼。
with concurrent.futures.ThreadPoolExecutor(max_workers=4) as executor: executor.map(process, works)
這里我們創建了一個線程池,總共有4個線程可以分配使用。excuter.map()表示對 works 中的每一個元素,並發地調用函數 process()。
並發編程之Asyncio
下面我們在來學習一下並發編程的另一種實現形式--Asyncio。Asyncio是單線程的,它只有一個主線程,但是可以運行多個不同的任務(task),這些不同的任務,被一個叫做 event loop 的對象所控制。你可以把這里的任務,類比成多線程版本里的線程。
為了簡化講解這個問題,我們可以假設任務只有兩個狀態:一是預備狀態;二是等待狀態。所謂的預備狀態,是指任務目前空閑,但隨時待命准備運行。而等待狀態,是指任務已經運行,但正在等待外部的操作完成,比如 I/O 操作。在這種情況下,event loop 會維護兩個任務列表,分別對應這兩種狀態;並且選取預備狀態的一個任務,使其運行,一直到這個任務把控制權交還給 event loop 為止。當任務把控制權交還給 event loop 時,event loop會根據其是否完成,把任務放到預備或等待狀態的列表,然后遍歷等待狀態列表的任務,查看他們是否完成。如果完成,則將其放到預備狀態的列表;如果未完成,則繼續放在等待狀態的列表。而原先在預備狀態列表的任務位置仍舊不變,因為它們還未運行。這樣,當所有任務被重新放置在合適的列表后,新一輪的循環又開始了:event loop 繼續從預備狀態的列表中選取一個任務使其執行…如此周而復始,直到所有任務完成。
接下來我們看一下如何通過Asyncio來實現並發編程。
import asyncio import time async def process(work): await asyncio.sleep(2) print('process {}'.format(work)) async def process_works(works): tasks = [asyncio.create_task(process(work)) for work in works] await asyncio.gather(*tasks) def main(): works = [ 'work1', 'work2', 'work3', 'work4' ] start_time = time.time() asyncio.run(process_works(works)) end_time = time.time() print('use {} seconds'.format(end_time - start_time)) if __name__ == '__main__': main() ####輸出#### process work1 process work2 process work3 process work4 use 2.0058629512786865 seconds
到此為止,我們已經把python的兩種並發編程方式多線程和Asyncio都講完了。不過,遇到實際問題時,我們該如何進行選擇呢?總的來說我們應該遵循以下規范。
- 如何I/O負載高,並且I/O操作很慢,需要很多任務/線程協同實現,那么使用 Asyncio 更合適。
- 如何I/O負載高,並且I/O操作很快,只需要有限數量的任務/線程,那么使用多線程就可以了。
歡迎大家留言和我交流。
了解更多有趣內容,獲取更多資料,請關注公眾號“程序員學長”,回復“資料”即可。