Python 的协程
前言
最近在看部分Python源碼時, 發現了async 這個關鍵字. 查了一下發現了Python中的協程.
協程這玩意, 在GO中我用過啊, 簡單說, 就是一個輕量級的線程嘛, 由語言自己來實現不同協程的調度. 想著Python中可能也是差不多的東西吧. 但是我Google搜了一下, 前面的說明都給出了下面的例子:
def consumer():r = ''while True:n = yield rif not n:returnprint('[CONSUMER] Consuming %s...' % n)r = '200 OK'def produce(c):c.send(None)n = 0while n < 5:n = n + 1print('[PRODUCER] Producing %s...' % n)r = c.send(n)print('[PRODUCER] Consumer return: %s' % r)c.close()c = consumer() produce(c)這這這, 這是協程咩? 這不就是一個生成器咩?
不過你想一下生成器的特征:
- 需要的時候, 返回一個值, 并將當前函數內部的狀態保存
- 下次執行的時候, 會恢復上次執行的環境并繼續執行
能夠保存運行狀態并在下次執行時恢復, 說他是協程貌似也沒什么問題哈. 而且, Python中的協程是跑在同一個線程中, 也就是串行執行的, 所以也不需要加鎖.
協程
看了上面的生成器, 是不是有一種想法? 這玩意不就是個生成器么? 為什么要叫協程? 沒錯, 就是生成器.
上面也說了, 協程的特點, 就是可以停止當前函數的執行并保存當前狀態, 并在下次執行時進行恢復. 對于單線程的運行來說, 什么時候需要這種操作呢? 等待的時候. 比如等待文件打開, 等待鎖, 等待網絡返回等等. 這時程序運行著也沒什么事做, 就可以先去做其他事情, 等這邊好了再繼續回來執行. 下面以單純的sleep舉例.
自己實現
我們如何使用原生的yield生成器來實現一個任務隊列呢? 我隨便寫了一下:
import timedef yield_sleep(delay):start_time = time.time()while True:if time.time() - start_time < delay:yieldelse:breakdef hello(name):print(f"{name}-1-{time.strftime('%X')}")yield from yield_sleep(1)print(f"{name}-2-{time.strftime('%X')}")# 創建任務 tasks = [hello('one'), hello('two')] while True:if len(tasks) <= 0:break# 復制數組, 下方刪除時能夠正常遍歷copy_tasks = tasks[:]for task in copy_tasks:try:next(task)except StopIteration:# 迭代完成, 刪除元素tasks.remove(task)簡單解釋一下, 我們在每次任務調用yield臨時返回時, 進行任務的輪換. 這樣, 原本需要2s 執行的操作, 一共只需要1s 即可. (這里為了說明效果, 只是簡單實現了一下. )
哎, 如此一來, 所有支持yield生成器的語言, 其實都是支持coroutine的呀, 比如我大 PHP, 嘿嘿.
asyncio
在Python 3.4中, 引入了asyncio包. 將異步 IO 的操作進行了封裝.
簡單說, asyncio內部, 維護了一個任務隊列, 在函數執行yield讓出執行權時, 切換到下一個任務繼續執行. 嗯, 大概就是這樣.
import asyncio import time@asyncio.coroutine def hello(name):print(f"{name}-1-{time.strftime('%X')}")yield from asyncio.sleep(1)print(f"{name}-2-{time.strftime('%X')}")# 獲取事件隊列 loop = asyncio.get_event_loop() # 并發執行任務 tasks = [hello('one'), hello('two')] loop.run_until_complete(asyncio.wait(tasks)) loop.close()可以看到, 函數在首次執行到yield時, 進行了中斷并將執行權讓了出去.
需要注意的一點, Python的協程是需要手動讓出執行權的. 這點與Go不同. 也就是說, 發生協程切換的時機為:
舉個例子(將上面的例子簡單修改):
import asyncio import time@asyncio.coroutine def hello(name, delay, not_yield):print(f"{name}-1-{time.strftime('%X')}")# 占用協程等待if not_yield:time.sleep(delay)else:yield from asyncio.sleep(1)print(f"{name}-2-{time.strftime('%X')}")# 獲取事件隊列 loop = asyncio.get_event_loop() # 并發執行任務 tasks = [hello('one', 2, False), hello('two', 5, True)] loop.run_until_complete(asyncio.wait(tasks)) loop.close()可以看到, 盡管協程1只是想等待1s, 但因為協程2一直占用這執行權沒有放出來, 故協程1等到協程2執行結束后才再次獲得執行權, 既5s 后
async/await
在Python 3.5中, 增加了async/await語法糖.
簡單來說, 將上面的@asyncio.coroutine換成async, 將yield from換成await就行了. 其他不變. 替換后, 上面的代碼就變成了這樣, 意思是一樣的.
import asyncio import timeasync def hello(name):print(f"{name}-1-{time.strftime('%X')}")await asyncio.sleep(1)print(f"{name}-2-{time.strftime('%X')}")# 獲取事件隊列 loop = asyncio.get_event_loop() # 并發執行任務 tasks = [hello('one'), hello('two')] loop.run_until_complete(asyncio.wait(tasks)) loop.close()通用方式
前面說的方式是簡單介紹下實現, 協程在Python中較為平常的使用方式如下:
import asyncio import timeasync def hello(name):print(f"{name}-1-{time.strftime('%X')}")await asyncio.sleep(1)print(f"{name}-2-{time.strftime('%X')}")async def main():"""創建任務并執行"""# 方案1: 添加并執行任務task1 = asyncio.create_task(hello('one'))task2 = asyncio.create_task(hello('tow'))# 調用`asyncio.create_task`方法調用后, 任務就已經存在調度隊列中了# 即使沒有手動`await`等待, 在協程切換時也會被執行. 我們添加`await`只是為了等待所有任務完成await task1await task2# 等待其中所有協程執行完成, 與分開 await 相同await asyncio.wait({task1, task2})# 等待協程執行. 若指定時間后還沒有執行完畢, 則會拋出異常await asyncio.wait_for(task1, 1)# 方案2: 批量執行協程. 方案1的簡化版# 返回值為所有協程的集合await asyncio.gather(hello('one'),hello('tow'))"""獲取任務信息"""# 獲取當前執行的任務current_task = asyncio.current_task()# 獲取事件循環中所有未完成的任務all_task = asyncio.all_tasks()# 關閉一個協程, 不再執行task1.cancel()# 獲取任務的結果. 若協程被關閉了, 會拋出異常ret = task1.result()asyncio.run(main())創建并執行一個協程任務, 這樣就不用操心事件隊列的問題了, 我們在main函數中要做的就是創建任務并執行.
注意, 使用關鍵字async定義的方法, 是一個協程對象, 不能單純調用hello('1234'), 其實現是一個裝飾器, 返回的是一個coroutine對象.
對于Python中的協程, 差不多就是這么個東西了. 簡單說, 就是在執行 IO 耗時操作時, 將執行權暫時讓出, 以活動更好的執行效率.
但是問題來了, 對于這種耗時操作, 我們總不能每次都自己實現一遍吧. 勿慌, 其實很多異步操作, 都已經有了實現, 具體可見: https://github.com/aio-libs, 列出來當前已經實現異步操作的大部分庫. 當然了, 系統asyncio庫中也有部分簡單的異步操作實現.
以后再寫耗時操作的時候, 就可以用上協程了, 比如爬蟲. 爬蟲在發起請求的時候, 是需要等待返回的, 這時候同時發起 n 個請求, 就可以極大的提高爬蟲的效率.
總結
以上是生活随笔為你收集整理的Python 的协程的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: java gui 单选_java GUI
- 下一篇: websocket python爬虫_p