Python 中的協程 (5) 無阻塞


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()

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

上面的代碼中,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))

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM