【python內功修煉011】:Python進程池和線程池詳解

一、關於線程池\進程池介紹

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)
    
    '''

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章