本節介紹asyncio剩餘的一些常用操作:事件循環實現無限循環任務,在事件循環中執行普通函數以及協程鎖。
一. 無限循環任務
事件循環的run_until_complete方法運行事件循環時,當其中的全部任務完成後,會自動停止循環;若想無限運行事件循環,可使用asyncio提供的run_forever方法:
import asyncio
import time
from datetime import datetime
async def work(loop, t):
print(datetime.strftime(datetime.now(), '%Y-%m-%d %H:%M:%S'), '[work] start')
await asyncio.sleep(t) # 模擬IO操作
print(datetime.strftime(datetime.now(), '%Y-%m-%d %H:%M:%S'), '[work] finished')
loop.stop() # 停止事件循環,stop後仍可重新運行
if __name__ == '__main__':
loop = asyncio.get_event_loop() # 創建任務,該任務會自動加入事件循環
task = asyncio.ensure_future(work(loop, 1))
print(datetime.strftime(datetime.now(), '%Y-%m-%d %H:%M:%S'), '[main]', task._state)
loop.run_forever() # 無限運行事件循環,直至loop.stop停止
print(datetime.strftime(datetime.now(), '%Y-%m-%d %H:%M:%S'), '[main]', task._state)
loop.close() # 關閉事件循環,只有loop處於停止狀態纔會執行
運行結果:使用loop.run_forever()啓動無限循環時,task實例會自動加入事件循環。如果註釋掉loop.stop()方法,則loop.run_forever()之後的代碼永遠不會被執行,因爲loop.run_forever()是個無限循環。
以上是單任務事件循環,將loop作爲參數傳入協程函數創建協程,在協程內部執行loop.stop方法停止事件循環。下面是多任務事件循環,使用回調函數執行loop.stop()停止事件循環:
import time
import asyncio
import functools
from datetime import datetime
def loop_stop(loop, future): # 最後一個參數必須爲future或task
print(datetime.strftime(datetime.now(), '%H:%M:%S'), '[callback] stop loop by callback.')
loop.stop()
async def work(t):
print(datetime.strftime(datetime.now(), '%H:%M:%S'), '[work] coroutine start.')
await asyncio.sleep(t)
print(datetime.strftime(datetime.now(), '%H:%M:%S'), '[work] coroutine end.')
def main():
loop = asyncio.get_event_loop()
tasks = asyncio.gather(work(3), work(1))
tasks.add_done_callback(functools.partial(loop_stop, loop))
loop.run_forever()
loop.close()
if __name__ == '__main__':
start = time.time()
main()
end = time.time()
print(f'耗時:{end-start}')
運行結果:asyncio.gather創建的蒐集器,參數爲任意數量的協程,任務蒐集器本身也是task / future對象。任務蒐集器的add_done_callback方法用來添加回調函數,該函數只在事件循環中所有的任務都完成後運行一次。注意,add_done_callback的參數爲回調函數,當回調函數定義了除future參數之外的任何參數後,必須使用偏函數。此處,使用functools.partial 方法創建偏函數以便將 loop 作爲參數加入回調函數。
loop.run_until_complete方法本身也是調用loop.run_forever方法,然後通過回調函數調用loop.stop實現。
二. 在事件循環中加入普函數
2.1 加入普通函數,並立即排定執行順序
事件循環的call_soon方法可以將普通函數作爲任務加入到事件循環,並立即排定任務的執行順序:
import asyncio
def func(name): # 普通函數
print(f'[func] hello, {name}')
async def work(t, name): # 協程函數
print(f'[work] {name} start.')
await asyncio.sleep(t)
print(f'[work] {name} finished.')
def main():
loop = asyncio.get_event_loop()
asyncio.ensure_future(work(3, 'A'))
loop.call_soon(func, 'word')
loop.create_task(work(2, 'B'))
loop.run_until_complete(work(3, 'C'))
if __name__ == '__main__':
main()
運行結果:loop.call_soon將普通函數當作task加入到事件循環並排定執行順序,該方法的第一個參數爲普通函數的名字,普通函數的參數寫在後面。loop.run_until_complete(work(3, 'C')),阻塞啓動事件循環,而且又添加了一個任務。
2.2 加入普通函數,並在稍後執行
loop.call_later方法同loop.call_soon一樣,可將普通函數作爲任務放到事件循環裏,不同之處在於,call_laster可設置延遲執行,第一個參數爲延遲時間:
import asyncio
def func(name): # 普通函數
print(f'[func] hello, {name}')
async def work(t, name): # 協程函數
print(f'[work] {name} start.')
await asyncio.sleep(t)
print(f'[work] {name} finished.')
def main():
loop = asyncio.get_event_loop()
asyncio.ensure_future(work(4, 'A'))
loop.call_later(1, func, 'word1')
loop.call_soon(func, 'word2')
loop.create_task(work(2, 'B'))
loop.call_later(3, func, 'word3')
loop.run_until_complete(work(2, 'C'))
if __name__ == '__main__':
main()
運行結果:work(2, 'C')完成時,輸出hello, word3的普通函數尚未執行,協程任務'A'仍處於暫停狀態。
2.3 其它常用方法
call_soon立即執行,call_later延遲執行,call_at在某時刻執行;loop.time是事件循環內部的一個即時方法,返回值是時刻,數據類型爲float。將上例中的call_later使用loop.time + call_at實現:
def main():
loop = asyncio.get_event_loop()
start = loop.time() # 時間循環內部時刻
asyncio.ensure_future(work(4, 'A'))
# loop.call_later(1, func, 'word1')
# 上面註釋這行等同於下面這行
loop.call_at(start+1, func, 'word1')
loop.call_soon(func, 'word2')
loop.create_task(work(2, 'B'))
# loop.call_later(3, func, 'word3')
loop.call_at(start+3, func, 'word3')
loop.run_until_complete(work(2, 'C'))
運行結果與1.2中的示例一致,不再贅述。這三個call_xxx方法的作用都是將函數作爲任務排定到事件循環中,返回值都是asyncio.events.TimerHandle實例,注意它們不是協程任務,不能作爲loop.run_until_complete的參數。
三. 協程鎖
asyncio.lock從字面意思來講,該被稱爲異步IO鎖,之所以叫協程鎖,是因爲它通常寫在子協程中,用來將協程內部的一段代碼鎖住,知道這段代碼運行完畢解鎖。協程鎖的固定用法是使用async with創建協程上下文環境,把需要加鎖的代碼寫入其中。(注:with 是普通上下文管理器關鍵字,async with 是異步上下文管理器關鍵字;能夠使用 with 關鍵字的對象須有 __enter__ 和 __exit__ 方法,而能夠使用 async with 關鍵字的對象須有 __aenter__ 和 __aexit__ 方法。async with 會自動運行 lock 的 __aenter__ 方法,該方法會調用 acquire 方法上鎖;在語句塊結束時自動運行 __aexit__ 方法,該方法會調用 release 方法解鎖。這和 with 一樣,都是簡化 try ... finally 語句)
import asyncio
l = []
lock = asyncio.Lock() # 協程鎖
async def coro_work(name):
print(f'coroutine {name} start.')
async with lock:
print(f'{name} run with lock start.')
if 'hi' in l:
return name
await asyncio.sleep(2)
l.append('hi')
print(f'{name} release lock.')
return name
async def one():
name = await coro_work('ONE')
print(f'{name} finished.')
async def two():
name = await coro_work('TWO')
print(f'{name} finished')
def main():
loop = asyncio.get_event_loop()
tasks = asyncio.wait([one(), two()])
loop.run_until_complete(tasks)
if __name__ == '__main__':
main()
運行結果: 當協程TWO運行到await asyncio.sleep(2)處時,將讓步CPU的使用權,協程ONE開始執行,但執行到async with lock時,會阻塞,因爲TWO還沒有釋放協程鎖,此刻線程進入阻塞狀態,開始等待TWO釋放協程鎖。鎖被釋放後,協程TWO結束運行,返回值作爲await coro_work('TWO')表達式的值,賦值給two中的局部變量name。至此協程ONE開始上鎖執行,由於此時if條件判斷返回True,將直接return,因此終端不會輸出鎖的釋放提示。
至此關於yield、yield from(await)、@asyncio.coroutine(async)、asyncio的介紹已經完畢,下一節將使用更多豐富的案例介紹協程的應用。