python多線程并發,python協程(4): asyncio結合多線程解決阻塞問題以及timer模擬

 2023-12-06 阅读 33 评论 0

摘要:一、異步方法依然會假死(freezing) ? 什么是程序的假死,這里不再多描述,特別是在編寫桌面程序的時候,如果是使用單個線程,同步函數的方式,假死是不可避免的,但是有時候我們即使是使用了異步函數的方式依然是不可避免

一、異步方法依然會假死(freezing)

?

什么是程序的假死,這里不再多描述,特別是在編寫桌面程序的時候,如果是使用單個線程,同步函數的方式,假死是不可避免的,但是有時候我們即使是使用了異步函數的方式依然是不可避免的,依然會假死,這是為什么呢,下面會通過幾個例子來詳細說明。

python多線程并發。1、一般程序的調用方“假死”

 
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
主函數執行
'''

注意一個問題:我們前面所講的例子中,沒有出現等待,是因為各個異步方法之間是“完全并列”關系,彼此之間沒有依賴,所以,我可以將所有的異步操作“gather”起來,然后通過事件循環,讓事件循環在多個異步方法之間來回調用,永不停止,故而沒有出現等待。

但是,現實中不可能所有的異步方法都是完全獨立的,沒有任何關系的,在上面的這個例子中,就是很好的說明,hello1是一個耗時任務,耗時大約3秒,main也是一個異步方法,但是main中需要用到hello1中的返回結果,所以他必須要等待hello1運行結束之后再才能繼續執行,這就是為什么會得到上面結果的原因。這也再一次說明,異步依然是會有阻塞的。

python協程asyncio?我們也可以這樣理解,因為我給事件循環只注冊了一個異步方法,那就是main,當在main里面遇到了await,事件循環掛起,轉而尋找其他的異步方法,但是由于只注冊了一個異步方法給事件循環,他沒有其他的方法可執行了,所以只能等待,讓hello1執行完了,再繼續執行。

2、窗體程序的假死

(1)同步假死

import tkinter as tk          # 導入 Tkinter 庫
import timeclass 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"]=300if __name__=='__main__':form=Form()

深入理解python異步編程。運行的結果就是,我單機一下“開始計算”按鈕,然后窗體會假死,這時候無法移動窗體、也無法最大化最小化、3秒鐘之后,“等待計算結果”的label會顯示出3,然后前面移動的窗體等操作接著發生,最終效果如下:

上面的窗體會假死,這無可厚非,因為,所有的操作都是同步方法,只有一個線程,負責維護窗體狀態的線程和執行好使計算的線程是同一個,當遇到time.sleep()的時候自然會遇到阻塞。那如果我們將耗時任務換成異步方法呢?代碼如下:

(2)異步假死

import tkinter as tk          # 導入 Tkinter 庫
import asyncioclass Form:def __init__(self):self.root=tk.Tk()self.root.geometry('500x300')self.root.title('窗體程序')  #設置窗口標題self.button=tk.Button(self.root,text="開始計算",command=self.get_loop)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#asyncio任務只能通過事件循環運行,不能直接運行異步函數def get_loop(self):self.loop=asyncio.get_event_loop()self.loop.run_until_complete(self.calculate())self.loop.close()if __name__=='__main__':form=Form()

我們發現,窗體依然會造成阻塞,情況和前面的同步方法是一樣的,為什么會這樣呢?因為這個地方雖然啟動了事件循環,但是擁有事件循環的那個線程同時還需要維護窗體的狀態,始終只有一個線程在運行,當單擊“開始計算”按鈕,開始執行get_loop函數,在get_loop里面啟動異步方法calculate,然后遇到await,這個時候事件循環暫停,但是由于事件循環只注冊了calculate一個異步方法,也沒其他事情干,所以只能等待,造成假死阻塞。

解決辦法就是我專門再創建一個線程去執行一些計算任務,維護窗體狀態的線程就之專門負責維護狀態,后面再詳說。

二、多線程結合asyncio解決調用時的假死

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、不阻塞的多線程并發實例

asyncio.run_coroutine_threadsafe(coroutine,loop)的意思很簡單,就是我在新線程中創建一個事件循環loop,然后在新線程的loop中不斷不停的運行一個或者是多個coroutine。參考下面代碼:

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函數運行結束
'''

我們發現,main是在主線程中的,而三個協程函數是在新線程中的,它們是在一起執行的,沒有造成主線程main的阻塞。下面再看一下窗體函數中的實現。

3、tkinter+threading+asyncio

import tkinter as tk          # 導入 Tkinter 庫
import time
import asyncio
import threadingclass 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"]=300def get_loop(self,loop):self.loop=loopasyncio.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()

運行上面的代碼,我們發現,此時點擊“開始計算”按鈕執行耗時任務,沒有造成窗體的任何阻塞,我可以最大最小化、移動等等,然后3秒之后標簽會自動顯示運算結果。為什么會這樣?

上面的代碼中,get_loop()、change_form_state()、__init__()都是定義在主線程中的,窗體的狀態維護也是主線程,二耗時計算calculate()是一個異步協程函數。

現在單擊“開始計算按鈕”,這個事件發生之后,會觸發主線程的chang_form_state函數,然后在該函數中,會創建新的線程,通過新的線程創建一個事件循環,然后將協程函數注冊到新線程中的事件循環中去,達到的效果就是,主線程做主線程的,新線程做新線程的,不會造成任何阻塞。

4、multithreading+asyncio總結

第一步:定義需要異步執行的一系列操作,及一系列協程函數;

第二步:在主線程中定義一個新的線程,然后在新線程中產生一個新的事件循環;

第三步:在主線程中,通過asyncio.run_coroutine_threadsafe(coroutine,loop)這個方法,將一系列異步方法注冊到新線程的loop里面去,這樣就是新線程負責事件循環的執行。

三、使用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 futurefuture.add_done_callback(function)def func(future):print('done')if __name__=='__main__':asyncio.run(timer(2,func))'''運行結果為:
done
done
done
done
done
done
done
done
done
done
done
.
.
.
.每隔2秒打印一個done
'''

幾個注意點:asyncio.sleep()本身就是一個協程函數,故而可以將它封裝成一個Task或者是Future,等待時間結束也就是任務完成,綁定回調函數。當然,本身python語法靈活,上面只是其中一種實現而已。

版权声明:本站所有资料均为网友推荐收集整理而来,仅供学习和研究交流使用。

原文链接:https://808629.com/189136.html

发表评论:

本站为非赢利网站,部分文章来源或改编自互联网及其他公众平台,主要目的在于分享信息,版权归原作者所有,内容仅供读者参考,如有侵权请联系我们删除!

Copyright © 2022 86后生记录生活 Inc. 保留所有权利。

底部版权信息