Develop with asyncio部分的翻譯

Develop with asyncio

異步程序和普通的連續程序(也就是同步程序)是很不同的,這裏會列出一些常見的陷阱,並介紹如何去避開他們。python

Debug mode of asyncio

咱們用asyncio就是爲了提升性能,而爲了更容易去開發編寫異步的代碼,咱們須要開啓debug模式安全

在應用中開啓調試模式:網絡

  • 全局開啓異步的調試模式,能夠經過設置環境變量PYTHONASYNCIODEBUG=1,或者直接調用AbstractEventLoop.set_debug()
  • 設置asynico logger的日誌等級爲DEBUG,如在代碼開頭logging.basicConfig(level=logging.DEBUG)
  • 配置warnings模塊去顯示ResourceWarning警告,如在命令行中添加-Wdefault這個選項去啓動python來顯示這些

Cancellation

取消任務(執行)這個操做對於普通的程序來說並不常見,可是在異步程序中,這不只是個很普通的事情,並且咱們還須要去準備好去處理它們。異步

能夠直接用Future.cancel()這個方法去取消掉Future類和tasks類,而wait_for這個方法則是直接在超時的時候取消掉這些任務。如下是一些間接取消任務的狀況async

若是在Future被取消以後調用它的set_result()或者set_exception(),它將會被一個異常致使執行失敗(後半句原句爲: it would fail with an exception)函數

if not fut.cancelled():
    fut.set_result('done')

不要直接經過AbstractEventLoop.call_soon()去安排future類調用set_result()或者set_exception(),由於這個future類能夠在調用這些方法前被取消掉oop

若是你等待一個future類的返回,那麼就應該提前檢查這個future類是否被取消了,這樣能夠避免無用的操做性能

@coroutine
def slow_operation(fut):
    if fut.cancelled():
        return
    # ... slow computtaion ...
    yield from fut
    # ...

shield()方法可以用來忽略取消操做ui

Concurrency and multithreading

一個事件循環是跑在一個線程上面的,它全部的回溯函數和任務也是執行在這個線程上的。當事件循環裏面跑着一個任務的時候,不會有其餘任務同時跑在同一個線程裏,可是當這個任務用了yield from以後,那麼這個任務將會被掛起,並而事件循環會去執行下一個任務。命令行

在不是同一線程的時候,應該用AbstractEventLoop.call_soon_threadsafe()方法來安排回溯。

loop.call_soon_threadsafe(callback, *args)

大多數asyncio對象都不是線程安全的。因此須要注意的是是否有在事件循環以外調用這些對象。舉個栗子,要取消一個future類的時候,不該該直接調用Future.cancel(),而是loop.call_soon_threadsafe(fut.cancel)

爲了控制信號和執行子進程,事件循環必須運行在主線程。

在不一樣線程安排回溯對象時,應該用run_coroutine_threadsafe(),它會返回一個concurrent.futures.Future對象去拿到結果

future = asyncio.run_coroutine_threadsafe(coro_func(), loop)
result = future.result(timeout)  # wait for the result with a timeout

AbstractEventLoop.run_in_executor()可以用來做爲線程池的執行者,在不一樣的線程裏去執行回溯,並且不會阻塞當前線程的事件循環。

Handle blocking functions correctly

阻塞函數不該該被直接調用。舉個栗子,若是一個函數被阻塞了一秒,那麼其餘任務都會被延遲一秒,這會產生很大的影響。

在網絡和子進程方面,asynico提供了高級的API如協議。

AbstractEventLoop.run_in_executor()方法可以在不阻塞當前線程的事件循環的前提下,去調用其餘線程或者進程的任務。

Logging

asyncio模塊的日誌信息在logging模塊的asyncio實例裏

默認的日誌等級是info,能夠根據本身的須要改變

logging.getLogger('asyncio').setLevel(logging.WARNING)

Detect coroutine objects never scheduled

當一個協程函數被調用,而後若是它的返回值沒有傳給ensure_future()或者AbstractEvenLoop.create_task()

的話,執行這個操做的協程對象將永遠不會被安排執行,這可能就是一個bug,能夠開啓調試模式去經過warning信息找到它。

舉個栗子

import asyncio
@asyncio.coroutine
def test():
    print('never scheduled')
   
test()

在調試模式下會輸出

Coroutine test() at test.py:3 was never yielded from
Coroutine object create at (most recent call last):
    File 'test.py', line 7, in <module>
      test()

解決方法就是去調用ensure_future()函數或者經過協程對象去調用AbstractEventLoop.create_task()

Detect exceptions never consumed

python常常會調用sys.displayhook()來處理那些未經處理過的異常。若是Future.set_exception()被調用,那麼這個異常將不會被消耗掉(處理掉),sys.displayhook()也不會被調用。取而代之的是,當這個future類被垃圾回收機制刪除的時候,日誌將會輸出這個異常的相關錯誤信息。

舉個栗子,unhandled exception

import asyncio

@asyncio.coroutine
def bug():
    raise Exception('not consumed')
    
loop = asyncio.get_event_loop()
asyncio.ensure_future(bug())
loop.run_forever()
loop.close()

輸出將是

Task exception was never retrieved
future: <Task finished coro=<coro() done, defined at asyncio/coroutines.py:139> exception=Exception('not consumed',)>
Traceback (most recent call last):
  File "asyncio/tasks.py", line 237, in _step
    result = next(coro)
  File "asyncio/coroutines.py", line 141, in coro
    res = func(*args, **kw)
  File "test.py", line 5, in bug
    raise Exception("not consumed")
Exception: not consumed

開啓asyncio的調試模式後,會獲得具體位置的錯誤信息

Task exception was never retrieved
future: <Task finished coro=<bug() done, defined at test.py:3> exception=Exception('not consumed',) created at test.py:8>
source_traceback: Object created at (most recent call last):
  File "test.py", line 8, in <module>
    asyncio.ensure_future(bug())
Traceback (most recent call last):
  File "asyncio/tasks.py", line 237, in _step
    result = next(coro)
  File "asyncio/coroutines.py", line 79, in __next__
    return next(self.gen)
  File "asyncio/coroutines.py", line 141, in coro
    res = func(*args, **kw)
  File "test.py", line 5, in bug
    raise Exception("not consumed")
Exception: not consumed

能夠看到第二行那裏,指出了拋出異常的位置

如下提供了幾個方法來解決這個問題。第一個方法就是將這個協程鏈到另一個協程並使用try/except去捕捉

@asyncio.coroutine
def handle_exception():
    try:
        yield from bug()
    except Exception:
        print("exception consumed")

loop = asyncio.get_event_loop()
asyncio.ensure_future(handle_exception())
loop.run_forever()
loop.close()

第二個方法就是用AbstractEventLoop.run_until_complete()

task = asyncio.ensure_future(bug())
try:
    loop.run_until_complete(task)
except Exception:
    print("exception consumed")

Chain corotuines correctly

當一個協程函數被另一個協程函數(任務)調用,他們之間應該明確地用yield from連接起來,否則的話,執行順序不必定和預想一致。

舉個栗子,用asyncio.sleep()模仿的慢操做致使的bug

import asyncio

@asyncio.coroutine
def create():
    yield from asyncio.sleep(3.0)
    print('(1) create file')
    
@asyncio.coroutine
def write():
    yield from asyncio.sleep(1.0)
    print('(2) write into file')
    
@asyncio.coroutine
def close():
    print('(3) close file')
    
@asyncio.coroutine
def test():
    asyncio.ensure_future(create())
    asyncio.ensure_future(write())
    asyncio.ensure_future(close())
    yield from asyncio.sleep(2.0)
    loop.stop()
    
loop = asyncio.get_event_loop()
asyncio.ensure_future(test())
loop.run_forever()
print('Pending tasks at exit: %s' % asyncio.Task.all_tasks(loop))
loop.close()

預想的輸出爲

(1) create file
(2) write into file
(3) close file
Pending tasks at exit: set()

實際的輸出爲

(3) close file
(2) write into file
Pending tasks at exit: {<Task pending create() at test.py:7 wait_for=<Future pending cb=[Task._wakeup()]>>}
Task was destroyed but it is pending!
task: <Task pending create() done at test.py:5 wait_for=<Future pending cb=[Task._wakeup()]>>

事件循環在create()完成前就已經中止掉了,並且close()write()以前被調用,然而咱們所但願調用這些函數的順序爲create()write()close()

爲了解決這個問題,必需要用yield from來處理這些任務

@asyncio.corotine
def test():
    yield from asyncio.ensure_future(create())
    yield from asyncio.ensure_future(write())
    yield from asyncio.ensure_future(close())
    yield from asyncio.sleep(2.0)
    loop.stop()

或者能夠不要asyncio.ensure_future()

@asyncio.coroutine
def test():
    yield from create()
    yield from write()
    yield from close()
    yield from asyncio.sleep(2.0)
    loop.stop()

Pending task destroyed

若是掛起的任務被摧毀掉的話,那麼包裹它的協程的執行操做將不會完成。這頗有可能就是個bug,因此會被warning級別日誌記錄到

舉個栗子,相關的日誌

Task was destroyed but it is pending!
task: <Task pending coro=<kill_me() done, defined at test.py:5> wait_for=<Future pending cb=[Task._wakeup()]>>

開啓asyncio的調試模式就能夠拿到在任務被建立出來的具體位置的錯誤信息,開啓調試模式後的相關日誌

Task was destroyed but it is pending!
source_traceback: Object created at (most recent call last):
  File "test.py", line 15, in <module>
    task = asyncio.ensure_future(coro, loop=loop)
task: <Task pending coro=<kill_me() done, defined at test.py:5> wait_for=<Future pending cb=[Task._wakeup()] created at test.py:7> created at test.py:15>

Close transports and event loops

當一個傳輸(交互)再也不須要的時候,調用它自身的close()方法來釋放內存,事件循環也必須明確地關閉掉(也就是要調用事件循環自身的close()

若是transport或者event loop沒有被顯示地關閉掉,ResourceWarning的警告信息會傳給負責摧毀它的那個執行構件。默認狀況下,ResourceWarning的警告信息會被忽略掉。

相關文章
相關標籤/搜索