玩轉python(7)python多協程,多線程的比較
前段時間在做一個項目,項目本身沒什麽難度,只是數據存在一個數據接口服務商那兒,這就意味著,前端獲取數據需要至少兩次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多協程,多線程的比較