前言 首先描述下業務場景,有一個介面,其中存在耗時操作,耗時操作的執行結果將寫入數據表中,不需要通過該介面直接返回。 因此理論上該介面可以在耗時操作執行結束前返回。本文中使用多線程後臺運行耗時操作,主線程提前返回,實現介面的提前返回。 此外,還嘗試使用協程實現,經驗證,協程適用於多任務併發處理,遇到 ...
前言
首先描述下業務場景,有一個介面,其中存在耗時操作,耗時操作的執行結果將寫入數據表中,不需要通過該介面直接返回。
因此理論上該介面可以在耗時操作執行結束前返回。本文中使用多線程後臺運行耗時操作,主線程提前返回,實現介面的提前返回。
此外,還嘗試使用協程實現,經驗證,協程適用於多任務併發處理,遇到耗時操作時自動切換任務,可以縮短多任務總的執行用時,而無法縮短單任務的執行用時,無法實現提前返回,因此不適用該場景。
1 同步任務
如下所示,定義任務,模擬耗時操作。
Python學習交流Q群:906715085### import time def task(): print("task start") print("sleep...") time.sleep(10) print("task end")
如下所示,main 函數中執行任務,return true 用於模擬介面的返回。
如果最後執行 print(“task_result={}”.format®) 表明是同步執行,否則是非同步執行。
def main(): print("main start") task() print("main end") return True if __name__ == '__main__': r = main() print("task_result={}".format(r))
執行結果如下所示,表明是同步執行。
main start
task start
sleep...
task end
main end
task_result=True
對於介面來說,響應時間是非常重要的性能指標,因此可以通過非同步執行實現介面的提前返回,進而降低介面響應時間。
2 非同步任務
本文中提到的非同步執行指的是後臺運行某些代碼或功能,不阻塞主程式。
非同步執行的常規實現方式是使用多線程/多進程。
2.1 多線程
可以通過多線程的方式實現非同步執行,前提是不執行 join() 方法,否則將阻塞主線程。
註意使用多線程實現非同步操作時,需要將任務函數作為參數傳給線程的構造函數,因此需要修改代碼中任務函數的調用方式。
編寫併發代碼時經常這樣重構:把依序執行的for迴圈體改成函數,以便併發執行。
如下所示,定義 run_task 函數,並將原有的任務函數 task 作為參數傳入,其中創建子線程執行任務函數,調用 start() 方法啟動線程,不調用 join() 方法主線程繼續向下執行。
from threading import Thread def run_task(f, *args, **kwargs): t = Thread(target=f, args=args, kwargs=kwargs) t.start() def main(): print("main start") run_task(task) print("main end") return True if __name__ == '__main__': r = main() print("task_result={}".format(r))
執行結果如下所示,表明是非同步執行,main 函數在任務函數 task 執行結束前返回。
main start task start main end sleep... task_result=True task end
直接通過多線程實現非同步任務的缺點是需要修改任務函數的調用方式,代碼的改動較大。
實際上可以通過 多線程+裝飾器 的方式將多線程包裝一層,在不改變任務函數調用方式的前提下實現非同步操作。
2.2 多線程 + 裝飾器
定義裝飾器,其中創建並啟動子線程,用於執行傳入的任務函數。本質上與上述多線程的實現相同。
def async_thread(f): def wrapper(*args, **kwargs): t = Thread(target=f, args=args, kwargs=kwargs) t.start() return wrapper
將裝飾器置於任務函數的定義處,在不修改原函數定義的前提下,在代碼運行期間動態增加功能。
如下所示,通過 async_thread 將同步操作修改為非同步操作,同時不修改任務函數的調用方式。
執行函數時,調用 task() 而非 run_task()。
@async_thread def task(): print("task start") print("sleep...") time.sleep(10) print("task end") def main(): print("main start") # run_task(task) task() print("main end") return True
執行結果如下所示,同樣是非同步執行。
main start task start sleep... main end task_result=True task end
3 非同步框架
Python支持多種非同步框架,如 Cerely、Tornado、Twisted 等,後續詳細介紹。
4 協程
同樣,Python支持協程的多種調用方法。本文中使用 asyncio 模塊通過協程實現非同步操作。
4.1 單任務
如下所示,task 任務與同步代碼的區別包括:
•調用 asyncio 模塊的 @asyncio.coroutine 裝飾器,將生成器聲明為協程;
•使用 yield from 語法,等待另外一個協程的返回;
•使用 asyncio.sleep() 代替 time.sleep(),其中 time.sleep() 阻塞,asyncio.sleep() 非阻塞,可以切換任務。
import asyncio @asyncio.coroutine def task(): print("task start") print("sleep...") yield from asyncio.sleep(10) print("task end")
main 方法與同步代碼的區別包括:
•調用 asyncio.get_event_loop() 創建事件迴圈,事件迴圈用於運行非同步任務和回調;
•調用 loop.run_until_complete() 將協程對象交給事件迴圈,並阻塞直到協程運行結束才返回;
•調用 loop.close(),關閉事件迴圈。
def main(): print("main start") loop = asyncio.get_event_loop() c = task() loop.run_until_complete(c) loop.close() print("main end") return True
執行結果如下所示,表明是同步執行。
main starttask startsleep…task endmain endtask_result=True
協程同步執行的原因是 asyncio 是一個基於事件迴圈的非同步IO模塊,其中通過 yield from 將協程的 async.sleep() 的控制權交給事件迴圈,然後掛起協程,也就是說 yield from 等待協程 async.sleep() 的返回結果。等待過程中讓出CPU執行權,由事件迴圈決定何時喚醒 async.sleep(),接著向後執行代碼。
可見,協程的作用體現在切換,切換生效的前提是存在多任務,當前代碼中只有一個任務,因此體現不出協程的作用。
4.2 多任務
如下所示創建多任務,並將協程對象交給事件迴圈。
def main(): print("main start") loop = asyncio.get_event_loop() # c = task() # loop.run_until_complete(c) c1 = task() c2 = task() loop.run_until_complete(asyncio.wait([c1, c2])) loop.close() print("main end") return True
執行結果如下所示,第二個任務的 task start 在第一個任務的 task end 之前執行,表明任務已切換。
main start
task start
sleep...
task start
sleep...
task end
task end
main end
task_result=True
同時,與協程執行單任務相同,task_result=True 也是最後執行,可見協程並不適用於函數的提前返回。
因此,可以根據場景選擇使用多線程/多進程與協程。
•多線程/多進程,適用於後臺執行,函數提前返回的場景;
•協程,適用於多任務併發執行,可以降低IO等待,最終降低多任務總的執行用時,無法降低單任務的執行用時。
當然,多線程/多進程也可以用於併發執行,需要根據具體場景選擇使用哪種方式實現。
5 結論
本文中的業務場景是實現介面的提前返回,後臺運行耗時操作。
本文中提到的非同步執行指的是後臺運行某些代碼或功能,不阻塞主程式。
非同步執行的常規實現方式是使用多線程/多進程,缺點是需要將原函數作為參數傳遞給線程/進程,因此需要修改調用的函數名。
結合裝飾器可以實現在不改變任務函數調用方式的前提下實現非同步操作。
此外,使用協程也可以實現非同步執行。但是協程適用於多任務併發處理,遇到耗時操作時自動切換任務,可以縮短多任務總的執行用時,而無法縮短單任務的執行用時,無法實現提前返回,因此不適用該場景。
事實上,Python中由於GIL的存在,在一個進程中每次只能有一個線程在運行,因此多線程處理併發的實現方式也是任務線程的切換。某個線程想要運行,首先要獲得GIL鎖,然後遇到IO或者超時的時候釋放GIL鎖,給其餘的線程去競爭,競爭成功的線程獲得GIL鎖得到下一次運行的機會。
因此,Python中多線程適用於IO密集型應用。
多進程處理併發的實現方式與CPU的核數有關。對於單核CPU,一個時間點只能運行一個進程,因此只能併發,無法並行,多進程通過時間片輪轉的方式輪流占用CPU。對於多核CPU,一個時間點每個CPU可以運行一個進程,因此可以實現並行。
因此,Python中多進程適用於CPU密集型應用。
多線程/多線程與協程的相同點是都可以實現任務的切換,不同點是切換機制的實現方式。
一個程式想要同時處理多個任務,必須提供一種能夠記錄任務執行進度的機制。多線程/多線程由CPU提供該機制,協程由事件迴圈提供。
6 小技巧
裝飾器+多線程 可用於比較優雅地實現非同步任務,不阻塞主程式。
協程的優勢在於多任務併發處理,可以實現輕量級的任務切換。
因此,使用過程中需要先確認業務場景,如果目標是提起返回,可以使用多線程,如果目標是多任務併發處理,可以使用協程。
今天分享的就那麼多,到這裡就沒有了,喜歡的點贊收藏,不懂的評論留言,一般我看見都會回覆的。下一篇見啦。