前言
首先描述下業務場景,有一個介面,其中存在耗時操作,耗時操作的執行結果將寫入資料表中,不需要通過該介面直接回傳,
因此理論上該介面可以在耗時操作執行結束前回傳,本文中使用多執行緒后臺運行耗時操作,主執行緒提前回傳,實作介面的提前回傳,
此外,還嘗試使用協程實作,經驗證,協程適用于多任務并發處理,遇到耗時操作時自動切換任務,可以縮短多任務總的執行用時,而無法縮短單任務的執行用時,無法實作提前回傳,因此不適用該場景,
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 小技巧
裝飾器+多執行緒 可用于比較優雅地實作異步任務,不阻塞主程式,
協程的優勢在于多任務并發處理,可以實作輕量級的任務切換,
因此,使用程序中需要先確認業務場景,如果目標是提起回傳,可以使用多執行緒,如果目標是多任務并發處理,可以使用協程,
今天分享的就那么多,到這里就沒有了,喜歡的點贊收藏,不懂的評論留言,一般我看見都會回復的,下一篇見啦,

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/453771.html
標籤:Python
下一篇:python函式的兩種嵌套方法
