1 異步程序依然會假死 freezing
1)一般程序的調用方 freezing
import asyncio import time import threading #定義一個異步操作 async def hello1(a,b): print(f"異步函數開始執行") await asyncio.sleep(3) print("異步函數執行結束") return a+b #在一個異步操作里面調用另一個異步操作 async def main(): c=await hello1(10,20) print(c) print("主函數執行") loop = asyncio.get_event_loop() tasks = [main()] loop.run_until_complete(asyncio.wait(tasks)) loop.close() '''運行結果為: 異步函數開始執行(在此處要等待3秒) 異步函數執行結束 30 主函數執行 '''
上面的例子中,hello1是一個耗時3s的異步任務,main也是一個異步方法,但是main需要調用hello1的返回值,所以必須登台hello1執行完成才能繼續執行main,這說明異步也是會有阻塞的。
而之前定義的異步函數不用等待是因為事件循環將所有的異步操作‘gather’起來,在多個操作間不同的游走切換,來回調用所有沒有等待。
也可以理解為,事件循環只有一個異步操作在處理,沒有可以切換執行的目標,所以只能等待當前的操作完成。
2)窗體程序的freezing
同步freezing
import tkinter as tk # 導入 Tkinter 庫 import time class Form: def __init__(self): self.root=tk.Tk() self.root.geometry('500x300') self.root.title('窗體程序') #設置窗口標題 self.button=tk.Button(self.root,text="開始計算",command=self.calculate) self.label=tk.Label(master=self.root,text="等待計算結果") self.button.pack() self.label.pack() self.root.mainloop() def calculate(self): time.sleep(3) #模擬耗時計算 self.label["text"]=300 if __name__=='__main__': form=Form()
運行的結果就是,我單機一下“開始計算”按鈕,然后窗體會假死,這時候無法移動窗體、也無法最大化最小化、3秒鍾之后,“等待計算結果”的label會顯示出3,然后前面移動的窗體等操作接着發生。
上面的窗體會假死,這無可厚非,因為,所有的操作都是同步方法,只有一個線程,負責維護窗體狀態的線程和執行好使計算的線程是同一個,當遇到time.sleep()的時候自然會遇到阻塞。那如果我們將耗時任務換成異步方法。我們發現,窗體依然會造成阻塞,情況和前面的同步方法是一樣的,為什么會這樣呢?因為這個地方雖然啟動了事件循環,但是擁有事件循環的那個線程同時還需要維護窗體的狀態,始終只有一個線程在運行,當單擊“開始計算”按鈕,開始執行get_loop函數,在get_loop里面啟動異步方法calculate,然后遇到await,這個時候事件循環暫停,但是由於事件循環只注冊了calculate一個異步方法,也沒其他事情干,所以只能等待,造成假死阻塞。
2 多線程+asyncio 解決調用時freezing
1)asyncio專門實現Concurrency and Multithreading(多線程和並發)的函數介紹
為了讓一個協程函數在不同的線程中執行,我們可以使用以下兩個函數 (1)loop.call_soon_threadsafe(callback, *args),這是一個很底層的API接口,一般很少使用,本文也暫時不做討論。 (2)asyncio.run_coroutine_threadsafe(coroutine,loop) 第一個參數為需要異步執行的協程函數,第二個loop參數為在新線程中創建的事件循環loop,注意一定要是在新線程中創建哦,該函數的返回值是一個concurrent.futures.Future類的對象,用來獲取協程的返回結果。 future = asyncio.run_coroutine_threadsafe(coro_func(), loop) # 在新線程中運行協程
result = future.result() #等待獲取Future的結果
2)不阻塞的多線程並發實例
import asyncio import asyncio,time,threading #需要執行的耗時異步任務 async def func(num): print(f'准備調用func,大約耗時{num}') await asyncio.sleep(num) print(f'耗時{num}之后,func函數運行結束') #定義一個專門創建事件循環loop的函數,在另一個線程中啟動它 def start_loop(loop): asyncio.set_event_loop(loop) loop.run_forever() #定義一個main函數 def main(): coroutine1 = func(3) coroutine2 = func(2) coroutine3 = func(1) new_loop = asyncio.new_event_loop() #在當前線程下創建時間循環,(未啟用),在start_loop里面啟動它 t = threading.Thread(target=start_loop,args=(new_loop,)) #通過當前線程開啟新的線程去啟動事件循環 t.start() asyncio.run_coroutine_threadsafe(coroutine1,new_loop) #這幾個是關鍵,代表在新線程中事件循環不斷“游走”執行 asyncio.run_coroutine_threadsafe(coroutine2,new_loop) asyncio.run_coroutine_threadsafe(coroutine3,new_loop) for i in "iloveu": print(str(i)+" ") if __name__ == "__main__": main() '''運行結果為: i 准備調用func,大約耗時3 l 准備調用func,大約耗時2 o 准備調用func,大約耗時1 v e u 耗時1之后,func函數運行結束 耗時2之后,func函數運行結束 耗時3之后,func函數運行結束 '''
tkinter+threading+asyncio
import tkinter as tk # 導入 Tkinter 庫 import time import asyncio import threading class Form: def __init__(self): self.root=tk.Tk() self.root.geometry('500x300') self.root.title('窗體程序') #設置窗口標題 self.button=tk.Button(self.root,text="開始計算",command=self.change_form_state) self.label=tk.Label(master=self.root,text="等待計算結果") self.button.pack() self.label.pack() self.root.mainloop() async def calculate(self): await asyncio.sleep(3) self.label["text"]=300 def get_loop(self,loop): self.loop=loop asyncio.set_event_loop(self.loop) self.loop.run_forever() def change_form_state(self): coroutine1 = self.calculate() new_loop = asyncio.new_event_loop() #在當前線程下創建時間循環,(未啟用),在start_loop里面啟動它 t = threading.Thread(target=self.get_loop,args=(new_loop,)) #通過當前線程開啟新的線程去啟動事件循環 t.start() asyncio.run_coroutine_threadsafe(coroutine1,new_loop) #這幾個是關鍵,代表在新線程中事件循環不斷“游走”執行 if __name__=='__main__': form=Form()
上面的代碼中,get_loop()、change_form_state()、init()都是定義在主線程中的,窗體的狀態維護也是主線程,二耗時計算calculate()是一個異步協程函數。
現在單擊“開始計算按鈕”,這個事件發生之后,會觸發主線程的chang_form_state函數,然后在該函數中,會創建新的線程,通過新的線程創建一個事件循環,然后將協程函數注冊到新線程中的事件循環中去,達到的效果就是,主線程做主線程的,新線程做新線程的,不會造成任何阻塞。
第一步:定義需要異步執行的一系列操作,及一系列協程函數; 第二步:在主線程中定義一個新的線程,然后在新線程中產生一個新的事件循環; 第三步:在主線程中,通過asyncio.run_coroutine_threadsafe(coroutine,loop)這個方法,將一系列異步方法注冊到新線程的loop里面去,這樣就是新線程負責事件循環的執行。
3 使用asyncio實現一個timer 定時器
所謂的timer指的是,指定一個時間間隔,讓某一個操作隔一個時間間隔執行一次,如此周而復始。很多編程語言都提供了專門的timer實現機制、包括C++、C#等。但是 Python 並沒有原生支持 timer,不過可以用 asyncio.sleep 模擬。 大致的思想如下,將timer定義為一個異步協程,然后通過事件循環去調用這個異步協程,讓事件循環不斷在這個協程中反反復調用,只不過隔幾秒調用一次即可。 簡單的實現如下(本例基於python3.7):
async def delay(time): await asyncio.sleep(time) async def timer(time,function): while True: future=asyncio.ensure_future(delay(time)) await future future.add_done_callback(function) def func(future): print('done') if __name__=='__main__': asyncio.run(timer(2,func))