您好,登錄后才能下訂單哦!
這篇文章主要介紹“如何理解Python中的協程”,在日常操作中,相信很多人在如何理解Python中的協程問題上存在疑惑,小編查閱了各式資料,整理出簡單好用的操作方法,希望對大家解答”如何理解Python中的協程”的疑惑有所幫助!接下來,請跟著小編一起來學習吧!
我們曾經在golang關于goroutine的文章當中簡單介紹過協程的概念,我們再來簡單review一下。協程又稱為是微線程,英文名是Coroutine。它和線程一樣可以調度,但是不同的是線程的啟動和調度需要通過操作系統來處理。并且線程的啟動和銷毀需要涉及一些操作系統的變量申請和銷毀處理,需要的時間比較長。而協程呢,它的調度和銷毀都是程序自己來控制的,因此它更加輕量級也更加靈活。
協程有這么多優點,自然也會有一些缺點,其中最大的缺點就是需要編程語言自己支持,否則的話需要開發者自己通過一些方法來實現協程。對于大部分語言來說,都不支持這一機制。go語言由于天然支持協程,并且支持得非常好,使得它廣受好評,短短幾年時間就迅速流行起來。
對于Python來說,本身就有著一個GIL這個巨大的先天問題。GIL是Python的全局鎖,在它的限制下一個Python進程同一時間只能同時執行一個線程,即使是在多核心的機器當中。這就大大影響了Python的性能,尤其是在CPU密集型的工作上。所以為了提升Python的性能,很多開發者想出了使用多進程+協程的方式。一開始是開發者自行實現的,后來在Python3.4的版本當中,官方也收入了這個功能,因此目前可以光明正大地說,Python是支持協程的語言了。
生成器(generator)
生成器我們也在之前的文章當中介紹過,為什么我們介紹協程需要用到生成器呢,是因為Python的協程底層就是通過生成器來實現的。
通過生成器來實現協程的原因也很簡單,我們都知道協程需要切換掛起,而生成器當中有一個yield關鍵字,剛好可以實現這個功能。所以當初那些自己在Python當中開發協程功能的程序員都是通過生成器來實現的,我們想要理解Python當中協程的運用,就必須從最原始的生成器開始。
生成器我們很熟悉了,本質上就是帶有yield這個關鍵詞的函數。
def test(): n = 0 while n < 10: val = yield n print('val = {}'.format(val)) n += 1
這個函數當中如果沒有yield這個語句,那么它就是一個普通的Python函數。加上了val = yield n這個語句之后,它有什么變化呢?
我們嘗試著運行一下:
# 調用test函數獲得一個生成器 g = test() print(next(g)) print(next(g)) print(next(g))
得到這么一個結果:
輸出的0,1,2很好理解,就是通過next(g)返回的,這個也是生成器的標準用法。奇怪的是為什么val=None呢?val不應該等于n么?
這里想不明白是正常的,因為這里涉及到了一個新的用法就是生成器的send方法。當我們在yield語句之前加上變量名的時候,它的含義其實是返回yield之后的內容,再從外界接收一個變量。也就是說當我們執行next(g)的時候,會從獲取yield之后的數,當我們執行g.send()時,傳入的值會被賦值給yield之前的數。比如我們把執行的代碼改成這樣:
g = test() print(next(g)) g.send('abc') print(next(g)) print(next(g))
我們再來看執行的結果,會發現是這樣的:
第一行val不再是None,而是我們剛剛傳入的abc了。
隊列調度
生成器每次在執行到yield語句之后都會自然掛起,我們可以利用這一點來當做協程來調度。我們可以自己實現一個簡易的隊列來模擬這個過程。
首先我們聲明一個雙端隊列,每次從隊列左邊頭部獲取任務,調度執行到掛起之后,放入到隊列末尾。相當于我們用循環的方式輪詢執行了所有任務,并且這整個全程不涉及任何線程創建和銷毀的過程。
class Scheduler: def __init__(self): self._queue = deque() def new_task(self, task): self._queue.append(task) def run(self): while self._queue: # 每次從隊列左側獲取task task = self._queue.popleft() try: # 通過next執行之后放入隊列右側 next(task) self._queue.append(task) except StopIteration: pass sch = Scheduler() sch.new_task(test(5)) sch.new_task(test(10)) sch.new_task(test(8)) sch.run()
這個只是一個很簡易的調度方法,事實上結合上yield from以及send功能,我們還可以實現出更加復雜的協程調度方式。但是我們也沒有必要一一窮盡,只需要理解最基礎的方法就可以了,畢竟現在我們使用協程一般也不會自己實現了,都會通過官方原生的工具庫來實現。
@asyncio.coroutine
在Python3.4之后的版本當中,我們可以通過@asyncio.coroutine這個注解來將一個函數封裝成協程執行的生成器。
在吸收了協程這個概念之后,Python對生成器以及協程做了區分。加上了@asyncio.coroutine注解的函數稱為協程函數,我們可以用iscoroutinefunction()方法來判斷一個函數是不是協程函數,通過這個協程函數返回的生成器對象稱為協程對象,我們可以通過iscoroutine方法來判斷一個對象是不是協程對象。
比如我把剛剛寫的函數上加上注解之后再來執行這兩個函數都會得到True:
import asyncio @asyncio.coroutine def test(k): n = 0 while n < k: yield print('n = {}'.format(n)) n += 1 print(asyncio.iscoroutinefunction(test)) print(asyncio.iscoroutine(test(10)))
那我們通過注解將方法轉變成了協程之后,又該怎么使用呢?
一個比較好的方式是通過asynio庫當中提供的loop工具,比如我們來看這么一個例子:
loop = asyncio.get_event_loop() loop.run_until_complete(test(10)) loop.close()
我們通過asyncio.get_event_loop函數創建了一個調度器,通過調度器的run相關的方法來執行一個協程對象。我們可以run_until_complete也可以run_forever,具體怎么執行要看我們實際的使用場景。
async,await和future
從Python3.5版本開始,引入了async,await和future。我們來簡單說說它們各自的用途,其中async其實就是@asyncio.coroutine,用途是完全一樣的。同樣await代替的是yield from,意為等待另外一個協程結束。
我們用這兩個一改,上面的代碼就成了:
async def test(k): n = 0 while n < k: await asyncio.sleep(0.5) print('n = {}'.format(n)) n += 1
由于我們加上了await,所以每次在打印之前都會等待0.5秒。我們把await換成yield from也是一樣的,只不過用await更加直觀也更加貼合協程的含義。
Future其實可以看成是一個信號量,我們創建一個全局的future,當一個協程執行完成之后,將結果存入這個future當中。其他的協程可以await future來實現阻塞。我們來看一個例子就明白了:
future = asyncio.Future() async def test(k): n = 0 while n < k: await asyncio.sleep(0.5) print('n = {}'.format(n)) n += 1 future.set_result('success') async def log(): result = await future print(result) loop = asyncio.get_event_loop() loop.run_until_complete(asyncio.wait([ log(), test(5) ])) loop.close()
在這個例子當中我們創建了兩個協程,第一個協程是每隔0.5秒print一個數字,在print完成之后把success寫入到future當中。第二個協程就是等待future當中的數據,之后print出來。
在loop當中我們要調度執行的不在是一個協程對象了而是兩個,所以我們用asyncio當中的wait將這兩個對象包起來。只有當wait當中的兩個對象執行結束,wait才會結束。loop等待的是wait的結束,而wait等待的是傳入其中的協程的結束,這就形成了一個依賴循環,等價于這兩個協程對象結束,loop才會結束。
到此,關于“如何理解Python中的協程”的學習就結束了,希望能夠解決大家的疑惑。理論與實踐的搭配能更好的幫助大家學習,快去試試吧!若想繼續學習更多相關知識,請繼續關注億速云網站,小編會繼續努力為大家帶來更多實用的文章!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。