例項程式碼講解Python 執行緒池
大家都知道當任務過多,任務量過大時如果想提高效率的一個最簡單的方法就是用多執行緒去處理,比如爬取上萬個網頁中的特定資料,以及將爬取資料和清洗資料的工作交給不同的執行緒去處理,也就是生產者消費者模式,都是典型的多執行緒使用場景。
那是不是意味著執行緒數量越多,程式的執行效率就越快呢。
顯然不是。執行緒也是一個物件,是需要佔用資源的,執行緒數量過多的話肯定會消耗過多的資源,同時執行緒間的上下文切換也是一筆不小的開銷,所以有時候開闢過多的執行緒不但不會提高程式的執行效率,反而會適得其反使程式變慢,得不償失。
所以,如何確定多執行緒的數量是多執行緒程式設計中一個非常重要的問題。好在經過多年的摸索業界基本已形成一套預設的標準。
對於 CPU 密集型的計算場景,理論上將執行緒的數量設定為 CPU 核數就是最合適的,這樣可以將每個 CPU 核心的效能壓榨到極致,不過在工程上,執行緒的數量一般會設定為 CPU 核數 + 1,這樣在某個執行緒因為未知原因阻塞時多餘的那個執行緒完全可以頂上。
而對於 I/O 密集型的應用,就需要考慮 CPU 計算的耗時和 I/O 的耗時比了。如果 I/O 耗時和 CPU 耗時 為 1:1,那麼兩個執行緒是最合適的,因為當 A 執行緒做 I/O 操作時,B 執行緒執行 CPU 計算任務,當 B 執行緒做 I/O 操作時,A 執行緒執行 CPU 計算任務,CPU 和 I/O 的利用率都得到了百分百,完美。所以可以認為最佳執行緒數 = CPU 核數 * [1 +(I/O 耗時 / CPU 耗時]。
執行緒池
平時我們自己寫多執行緒程式時基本都是直接呼叫 Thread(target=method) 即可,實際上建立執行緒遠沒有這麼簡單,需要分配記憶體,同時執行緒還需要呼叫作業系統核心的 API,然後作業系統還需要為執行緒分配一系列的資源,過程很是複雜,所以要儘量避免頻繁的建立和銷燬執行緒。
回想一下自己平時寫多執行緒程式碼的模式,是不是當任務來臨時直接建立執行緒,執行任務,當任務執行結束之後,執行緒也就隨之消亡了。然後又開始迴圈往復。有多少個任務就建立了多少個執行緒。這種模式的話很浪費硬體資源。
那如何避免這種問題呢,執行緒池就派上用場了。
其實執行緒池就是生產者消費者模式的最佳實踐,當執行緒池初始化時,會自動建立指定數量的執行緒,有任務到達時直接從執行緒池中取一個空閒執行緒來用即可,當任務執行結束時執行緒不會消亡而是直接進入空閒狀態,繼續等待下一個任務。而隨著任務的增加執行緒池中的可用執行緒必將逐漸減少,當減少至零時,任務就需要等待了。
在 python 中使用執行緒池有兩種方式,一種是基於第三方庫 threadpool
,另一種是基於 python3 新引入的庫 concurrent.futures.ThreadPoolExecutor
。這裡我們都做一下介紹。
threadpool 方式
使用 threadpool 前需要先安裝一下,看了這麼久我們的文章,相信你很快就會搞定的。在命令列執行如下命令即可。
pip install threadpool
以下是一個簡易的執行緒池使用模版,我們建立了一個函式 sayhello
,然後建立了一個大小為 2 的執行緒池,也就是執行緒池總共有兩個活躍執行緒。
最後通過 pool.putRequest()
將任務丟到執行緒池執, pool.wait()
等待所有執行緒結束。同時我們還可以定義回撥函式,拿到任務的返回結果。
由結果我們可以看出,執行緒池中的確只有兩個執行緒,分別為 Thread-1
和 Thread-2
。
import time import threadpool import threading def sayhello(name): print("%s say Hello to %s" % (threading.current_thread().getName(),name)); time.sleep(1) return name def callback(request,result): # 回撥函式,用於取回結果 print("callback result = %s" % result) name_list =['admin','root','scott','tiger'] start_time = time.time() pool = threadpool.ThreadPool(2) # 建立執行緒池 requests = threadpool.makeRequests(sayhello,name_list,callback) # 建立任務 [pool.putRequest(req) for req in requests] # 加入任務 pool.wait() print('%s cost %d second' % (threading.current_thread().getName(),time.time()-start_time)) ## 執行結果如下 Thread-1 say Hello to admin Thread-2 say Hello to root Thread-1 say Hello to scott Thread-2 say Hello to tiger callback result = admin callback result = root callback result = tiger callback result = scott MainThread cost 2 second
ThreadPoolExecutor 方式
ThreadPoolExecutor
是 python3 新引入的庫,具體使用方法與 threadpool
大同小異,同樣是建立容量為 2 的執行緒池,提交四個任務。只不過這裡分別是通過 submit
和 as_completed
來提交和獲取任務返回結果的。
同樣由輸出結果我們可以看出,兩種執行緒池的實現方式中關於執行緒的命名方式是不一致的。
import time import threading from concurrent.futures import ThreadPoolExecutor,as_completed def sayhello(name): print("%s say Hello to %s" % (threading.current_thread().getName(),name)); time.sleep(1) return name name_list =['admin','tiger'] start_time = time.time() with ThreadPoolExecutor(2) as executor: # 建立 ThreadPoolExecutor future_list = [executor.submit(sayhello,name) for name in name_list] # 提交任務 for future in as_completed(future_list): result = future.result() # 獲取任務結果 print("%s get result : %s" % (threading.current_thread().getName(),result)) print('%s cost %d second' % (threading.current_thread().getName(),time.time()-start_time)) ## 執行結果如下 ThreadPoolExecutor-0_0 say Hello to admin ThreadPoolExecutor-0_1 say Hello to root ThreadPoolExecutor-0_0 say Hello to scott ThreadPoolExecutor-0_1 say Hello to tiger MainThread get result : root MainThread get result : tiger MainThread get result : scott MainThread get result : admin MainThread cost 2 second
執行緒池總結
本文介紹了常用的兩種執行緒池的實現方式,在多執行緒程式設計中能使用執行緒池就不要自己去建立執行緒,並不是說執行緒池實現的多麼好,其實我們自己完全也可以實現一個功能更強大的執行緒池。但是其內建的執行緒池一來是受過全方面測試的,在安全性,效能和方便性上基本就是最優的了,同時執行緒池還替我們做了很多額外的工作,比如任務佇列的維護,執行緒銷燬時資源的回收等都不需要開發者去關心,我們只需注重業務邏輯即可,不需要在關心其他額外的工作,這將大大提高我們的的工作效率和使用感受。
當然其自帶的執行緒池也不是十全十美的,至少暫時沒有提供動態新增任務的入口出來。而且在設計方面不夠靈活,比如我想執行緒池只維護一個核心數量,也就是上文說的最大數量。但是當任務過多時可以再額外創建出一些新的執行緒(閾值可以自定義),處理完之後這些多餘的執行緒將自動銷燬,目前這個是做不到的。
程式碼地址
https://github.com/JustDoPython/python-100-day/tree/master/day-053
參考資料
https://chrisarndt.de/projects/threadpool/api/
以上就是例項程式碼講解Python 執行緒池的詳細內容,更多關於Python 執行緒池的資料請關注我們其它相關文章!