正確合理地使用併發程式設計,無疑會給我們的程式帶來極大的效能提升。今天我就帶大家一起來剖析一下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操作很快,只需要有限數量的任務/執行緒,那麼使用多執行緒就可以了。
歡迎大家留言和我交流。
需要更多知識,請關注公眾號,老韓隨筆。