文章目錄
一、關於線程池\進程池介紹
1.1 池的概念
-
池是一組資源的集合,這組資源在程序啓動時就完全被創建並初始化,這也稱爲爲
靜態資源分配
。 -
程序從系統(內存)調用和分配資源,包括之後釋放資源都需要耗費大量時間。如果把相關資源放在“池“中,程序從池中獲取和釋放,無需動態分配,無疑速度要快很多。
-
池相當於服務器管理系統資源的應用設施,它避免了服務器對內核的頻繁訪問。
池的概念主要目的是爲了重用:讓線程或進程在生命週期內可以多次使用。它減少了創建創建線程和進程的開銷,“以空間換時間”,來提高了程序性能。重用不是必須的規則,但它是程序員在應用中使用池的主要原因。
1.2 池的劃分
池可以分爲多種,常見的有
內存池
、進程池
、線程池
和連接池
。
1.3 線程池和進程池的區別
線程池和進程池相似,用法基本一致。主要是應用場景的不同,在判斷對程序是否進行線程池或進程池操作時,得先看程序業務。
業務 | 用途 | 建議使用 |
---|---|---|
IO密集型 | 讀取文件,讀取網絡套接字頻繁等。 | 線程池 |
計算密集型 | 大量消耗CPU的數學與邏輯運算,也就是我們這裏說的平行計算。 | 進程池,利用硬件多核優勢 |
1.5 進程池的創建(流程)
- 創建進程池,在池內放入合適數量的進程
- 將事件加入進程池的等待隊列
- 使用進程池內的進程不斷的執行等待事件,直到所有事件執行完畢
- 所有事件處理完畢後,關閉進程池,回收進程池
二、創建線程池\進程池的兩種方法
從Python3.2python標準庫爲我們提供了concurrent和multiprocessing模塊編寫相應的異步多線程/多進程代碼。
2.1 concurrent和multiprocessing區別
兩個模塊本質區別並不大,有的也只是調用方式略有差異。先有的 multiprocessing,後有concurrent.futures,後者的出現就是爲了降低編寫代碼的難度,後者的學習成本較低。
本博文主要以介紹 concurrent.futures模塊爲主。
三、concurrent.futures模塊
3.1 模塊的介紹
從 Python3.2開始,Python 標準庫提供了 concurrent.futures 模塊,爲開發人員提供了啓動異步任務的高級接口。
concurrent.futures模塊的基礎是Exectuor,Executor是一個抽象類,它不能被直接使用。但是它提供的兩個子類ThreadPoolExecutor和ProcessPoolExecutor卻是非常有用,顧名思義兩者分別被用來創建線程池和進程池的代碼。我們可以將相應的tasks直接放入線程池/進程池,不需要維護Queue來操心死鎖的問題,線程池/進程池會自動幫我們調度。
3.2 Executor.submit 創建進程/線程池
進程/線程池,只有固定個數的線程/進程,通過 max_workers 指定。
-
任務通過 Executor.submit 提交到 executor 的任務隊列,返回一個 future 對象。
-
Future 是常見的一種併發設計模式。一個Future對象代表了一些尚未就緒(完成)的結果,在「將來」的某個時間就緒了之後就可以獲取到這個結果。
-
任務被調度到各個 workers 中執行。但是要注意:
- 一個任務一旦被執行,在執行完畢前,會一直佔用該 worker!
- 如果 workers 不夠用,其他的任務會一直等待! 因此 Executor不適合實時任務。
簡易創建進程池示例:
'''
Executor.submit 創建進程實例
'''
from concurrent.futures import ProcessPoolExecutor
import time, os
# 打印信息
def print_info(n):
print("%s: 開啓" % os.getpid())
time.sleep(1)
return n**2
if __name__ == '__main__':
pool = ProcessPoolExecutor(4) # 開啓四個進程
for i in range(10): # 執行10個任務
pool.submit(print_info, i)
四、concurrent.futures 常用模塊
concurrent.futures 包含三個部分的 API:
4.1 Executor模塊
也就是兩個執行器的 API
-
構造器:主要的參數是 max_workers,用於指定線程池大小(或者說 workers 個數)
-
submit(fn, *args, **kwargs) # 返回一個 future,用於獲取結果
- 將任務函數 fn 提交到執行器,args 和 kwargs 就是 fn 需要的參數。
-
map(func, *iterables, timeout=None, chunksize=1) # 返回一個 futures 的迭代器
- 當任務是同一個,只有參數不同時,可以用這個方法代替 submit。iterables 的每個元素對應 func 的一組參數。
-
shutdown(wait=True)
:關閉執行器,一般都使用 with 管理器自動關閉。
4.2 Future模塊
任務被提交給執行器後,會返回一個 future
函數 | 作用 |
---|---|
future.result(timout=None) | 最常用的方法返回任務的結果。如果任務尚未結束,這個方法會一直等待! |
-
timeout 指定超時時間,爲 None 時沒有超時限制。
-
exception(timeout=None)
:給出任務拋出的異常。和 result() 一樣,也會等待任務結束。 -
cancel()
:取消此任務 -
add_done_callback(fn)
:future 完成後,會執行fn(future)
。 -
running()
:是否正在運行 -
done()
:future 是否已經結束了,boolean -
…詳見官方文檔
4.3 模塊其他實用函數
函數 | 功能 |
---|---|
concurrent.futures.as_completed(fs, timeout=None) | 等待 fs (futures iterable)中的 future 完成 |
- 一旦 fs 中的某 future 完成了,這個函數就立即返回該 future。
- 這個方法,使每次返回的 future,總是最先完成的 future。而不是先等待任務 1,再等待任務 2…
- 常通過
for future in as_completed(fs):
使用此函數。
函數 | 功能 |
---|---|
concurrent.futures.wait(fs, timeout=None, return_when=ALL_COMPLETED) | 一直等待,直到 return_when 所指定的事發生,或者 timeout |
- return_when 有三個選項:ALL_COMPLETED(fs 中的 futures 全部完成),FIRST__COMPLETED(fs 中任意一個 future 完成)還有 FIRST_EXCEPTION(某任務拋出異常)
五、程序實例
5.1 進程池實例
from concurrent.futures import ProcessPoolExecutor
import os,time,random
def task(n):
print('%s is running' %os.getpid())
time.sleep(2)
return n**2
if __name__ == '__main__':
p = ProcessPoolExecutor() #不填則默認爲cpu的個數
# print(os.cpu_count()) # 獲取本地cpu個數
l=[]
start=time.time()
for i in range(10):
obj=p.submit(task,i) #submit()方法返回的是一個future實例,要得到結果需要用obj.result()
l.append(obj)
p.shutdown() #類似用from multiprocessing import Pool實現進程池中的close及join一起的作用
print('='*30)
# print([obj for obj in l])
print([obj.result() for obj in l])
print(time.time()-start)
#上面方法也可寫成下面的方法
# start = time.time()
# with ProcessPoolExecutor() as p: #類似打開文件,可省去.shutdown()
# future_tasks = [p.submit(task, i) for i in range(10)]
# print('=' * 30)
# print([obj.result() for obj in future_tasks])
# print(time.time() - start)
補充:
1、執行完了shutdown()方法之後,再運行 submit 就會報錯
2、concurrent.futures 基於 multiprocessing. Pool 實現,因此實際上它比直接使用 線程/進程 的 Pool 要慢一點。但是它提供了更方便簡潔的 API。
5.2 線程池實例
from concurrent.futures import ThreadPoolExecutor
import threading
import os, time
def task(n):
print('%s:%s is running' % (threading.currentThread().getName(),os.getpid()))
time.sleep(2)
return n**2
if __name__ == '__main__':
p = ThreadPoolExecutor() # 不填則默認爲cpu的個數*5
l = []
start = time.time()
for i in range(10): # 開啓10個任務
obj = p.submit(task,i)
l.append(obj)
p.shutdown()
print('='*30)
print([obj.result() for obj in l])
print(time.time()-start)
5.3 同步和異步的實例
- 同步調用:提交完任務,就在原地等,等任務執行完,拿到任務的返回值,才能繼續下一行代碼,導致程序串行執行
- 異步調用:提交完任務,不在原地等,程序是並行執行, 任務完成後會返回一個實例,必須通過特殊方法才能得到
線程池和進程池中默認的都是異步執行的。這樣能夠最大程度提高程序的執行速度。
異步回調實例
# 異步調用
def counter(x):
time.sleep(1)
return x**x
if __name__ == '__main__':
start = time.time()
p = ProcessPoolExecutor(4)
l = []
for i in range(1, 10):
results = p.submit(counter, i)
l.append(results)
print([i.result() for i in l])
p.shutdown()
print('花費時間:', time.time() - start)
print('運算結束!')
'''
結果:
[1, 4, 27, 256, 3125, 46656, 823543, 16777216, 387420489]
花費時間: 3.282705783843994
運算結束!
'''
同步回調實例
# 同步調用
def counter(x):
time.sleep(1)
return x**x
if __name__ == '__main__':
start = time.time()
p = ProcessPoolExecutor(4)
l = []
for i in range(1, 10):
res = p.submit(counter, i).result()
l.append(res)
print([i for i in l])
p.shutdown(wait=True)
print('花費時間:', time.time() - start)
print('運算結束!')
'''
結果:
[1, 4, 27, 256, 3125, 46656, 823543, 16777216, 387420489]
花費時間: 9.329394340515137
運算結束!
'''
5.4 Executor.map用法示例
from concurrent import futures
import time
def test(x):
time.sleep(2)
return time.ctime(), x
if __name__ == '__main__':
with futures.ThreadPoolExecutor() as T:
for i in T.map(test,[1,2,3]):
print(i)
# 等同於下面註釋代碼
# with futures.ThreadPoolExecutor() as T:
# l = []
# for i in [1, 2, 3]:
# obj = T.submit(test, i)
# l.append(obj)
#
# print([i.result() for i in l])
'''
結果:
('Tue Apr 7 00:12:17 2020', 1)
('Tue Apr 7 00:12:17 2020', 2)
('Tue Apr 7 00:12:17 2020', 3)
'''