1. 程式人生 > >玩轉python(7)python多協程,多線程的比較

玩轉python(7)python多協程,多線程的比較

用戶體驗 time() cut 過程 RR 至少 執行 結果 關鍵字

前段時間在做一個項目,項目本身沒什麽難度,只是數據存在一個數據接口服務商那兒,這就意味著,前端獲取數據需要至少兩次http請求,第一次是前端到後端的請求,第二次是後端到數據接口的請求。有時,後端接收到前端的一次請求後,可能需要對多個接口進行請求,按照傳統串行執行請求的方法,用戶體驗肯定是非常糟糕了,而且對計算資源也是極大的浪費,正好前段時間學習了協程和線程的知識,所以我花了一些時間,對幾種可行方案進行了測試對比。一開始我使用真正的網絡io進行測試,發現這種方法受網絡環境影響比較大,為了公平起見,用sleep(0.02)來代替網絡io,接下來介紹方案和測試結果。

基本方案:串行執行

import time

def wget(flag):
    time.sleep(0.02) #模擬網絡io
    print(flag)

count = 100 #進行100次請求
start = time.time() #開始時刻
for i in range(count):
    wget(i)
end = time.time() #結束時刻
cost = end - start #耗時
print(‘cost:‘ + str(spend))

最終耗時超過2s,毫無疑問,這是最沒效率的方案,僅僅是作為參照而已。

改進方案:多線程

多線程的計時方法顯然不能照搬串行執行的測試方案,原因在於每個線程啟動後,如果調用join()阻塞主線程,那麽相當於串行執行,如果不調用join(),那麽結束時刻end會在所有線程完成之前就返回,測試結果必然不準。所以我用了一個笨辦法:在每個線程結束時打印當前時間戳,把控制臺上最後一個時間戳減去線程開始執行的時間戳,就是運行耗時。

import time
import threading

mutex=threading.Lock() #初始化鎖對象

def wget():
    time.sleep(0.02) #模擬網絡io
    mutex.acquire() #加鎖
    print(‘endtime:‘+str(time.time())) #當前線程的結束時刻
    mutex.release() #釋放鎖

count = 100 #進行100次請求
start = time.time() #開始時刻
print(‘starttime:‘+str(start))
for i in range(count):
    t = threading.Thread(target=wget)
    t.start()

最後結果約為0.08s,這個成績顯然比串行執行好得多,不過程序的運行效率不會隨著線程數量的增長而線性增長,原因在於線程創建切換銷毀時的開銷,極端情況下會造成崩潰。如果線程數不可控制時,這種方案要慎用。

改進方案:線程池

為了解決上面提到的多線程的不足之處,這裏使用線程池。

import time
import threading
from concurrent.futures import ThreadPoolExecutor, as_completed, wait, ALL_COMPLETED

mutex=threading.Lock() #初始化鎖對象

def wget():
    time.sleep(0.02) #模擬網絡io
    mutex.acquire() #加鎖
    print(threading.currentThread())
    mutex.release() #釋放鎖

size = 40 #線程池大小
count = 100 #進行100次請求
start = time.time() #開始時刻
pool = ThreadPoolExecutor(max_workers=size) #線程池對象
tasks = [pool.submit(wget) for i in range(count)]
wait(tasks, return_when=ALL_COMPLETED) #等待所有線程完成
end = time.time() #結束時刻
print(‘spend:‘ + str(end-start))

經過測試後發現,當線程池線程數量設置為40時,耗時最小,約為0.08s,與上一個方案相當,不過隨著線程數量繼續增加,線程池穩定的特點會顯現出來。

改進方案:協程異步

import asyncio
import time

async def wget(flag): #async關鍵字表明這是一個異步操作
    await asyncio.sleep(0.02) #await相當於yield from
    print(flag)

count = 100 #進行100次請求
start = time.time() #開始時刻
loop = asyncio.get_event_loop() #事件循環對象
tasks = [wget(i) for i in range(count)]
loop.run_until_complete(asyncio.wait(tasks)) #等待所有協程完成
loop.close()
end = time.time() #結束時刻
print(‘spend:‘ + str(end - start))

這個方案的結果讓我相當震驚,沒有用到任何多線程技術,所有的操作在一個線程上完成,耗時0.06s,是所有方案中耗時最少的。協程更令人振奮的優點在於,協程創建的開銷與線程相比完全可以忽略,這意味著,使用多協程可以處理更多的任務。

總結

顯然在這幾個方案中,協程是最具有優勢的,將來如果有時間,我還會對多協程多進程的協同進行測試。

玩轉python(7)python多協程,多線程的比較