python實現協程(六)

        本節介紹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的介紹已經完畢,下一節將使用更多豐富的案例介紹協程的應用。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章