「ファイアアンドフォーゲット」python async / await


115

ときどき、重要でない非同期操作が発生することがありますが、それが完了するのを待ちたくありません。トルネードのコルーチンの実装では、yieldキーワードを省略するだけで非同期関数を「起動して忘れる」ことができます。

私は、Python 3.5でリリースされた新しいasync/ await構文を使用して「起動して忘れる」方法を理解しようと努めてきました。たとえば、簡略化されたコードスニペット:

async def async_foo():
    print("Do some stuff asynchronously here...")

def bar():
    async_foo()  # fire and forget "async_foo()"

bar()

何が起こるかというと、bar()決して実行されず、代わりにランタイム警告が表示されます。

RuntimeWarning: coroutine 'async_foo' was never awaited
  async_foo()  # fire and forget "async_foo()"

関連?stackoverflow.com/q/32808893/1639625実際、これは重複していると思いますが、インスタントハンマーで叩きたくありません。誰かが確認できますか?
tobias_k 2016年

3
@tobias_k、重複しているとは思わない。リンクでの回答が広すぎるため、この質問に回答できません。
ミハイルゲラシモフ2016年

2
(1)「メイン」プロセスは永久に実行し続けますか?または、(2)プロセスの終了を許可しますが、忘れられたタスクがジョブを続行できるようにしますか?または(3)終了する直前にメインプロセスが忘れられたタスクを待つことを好みますか?
ジュリアンパラード16年

回答:


170

更新:

Python> = 3.7を使用asyncio.ensure_futureasyncio.create_taskている場合は、あらゆる場所で置き換えてください。これは、taskを生成するための新しい、より良い方法です。


「起動して忘れる」ためのasyncio.Task

Pythonのドキュメントによると、「バックグラウンドで」実行するasyncio.Taskコルーチンを開始することが可能です。関数によって作成されたタスクは実行をブロックしません(したがって、関数はすぐに戻ります)。これは、要求どおりに「発砲して忘れる」方法のように見えます。asyncio.ensure_future

import asyncio


async def async_foo():
    print("async_foo started")
    await asyncio.sleep(1)
    print("async_foo done")


async def main():
    asyncio.ensure_future(async_foo())  # fire and forget async_foo()

    # btw, you can also create tasks inside non-async funcs

    print('Do some actions 1')
    await asyncio.sleep(1)
    print('Do some actions 2')
    await asyncio.sleep(1)
    print('Do some actions 3')


if __name__ == '__main__':
    loop = asyncio.get_event_loop()
    loop.run_until_complete(main())

出力:

Do some actions 1
async_foo started
Do some actions 2
async_foo done
Do some actions 3

イベントループの完了後にタスクが実行されている場合はどうなりますか?

asyncioは、イベントループが完了した瞬間にタスクが完了することを期待していることに注意してください。したがって、次のように変更main()する場合:

async def main():
    asyncio.ensure_future(async_foo())  # fire and forget

    print('Do some actions 1')
    await asyncio.sleep(0.1)
    print('Do some actions 2')

プログラムが終了すると、次の警告が表示されます。

Task was destroyed but it is pending!
task: <Task pending coro=<async_foo() running at [...]

これを防ぐには、イベントループが完了した後、保留中のすべてのタスクを待つだけです。

async def main():
    asyncio.ensure_future(async_foo())  # fire and forget

    print('Do some actions 1')
    await asyncio.sleep(0.1)
    print('Do some actions 2')


if __name__ == '__main__':
    loop = asyncio.get_event_loop()
    loop.run_until_complete(main())

    # Let's also finish all running tasks:
    pending = asyncio.Task.all_tasks()
    loop.run_until_complete(asyncio.gather(*pending))

タスクを待つのではなく殺す

タスクが完了するのを待ちたくない場合があります(たとえば、一部のタスクが永久に実行されるように作成される場合があります)。その場合、それらを待つのではなく、単にcancel()できます。

import asyncio
from contextlib import suppress


async def echo_forever():
    while True:
        print("echo")
        await asyncio.sleep(1)


async def main():
    asyncio.ensure_future(echo_forever())  # fire and forget

    print('Do some actions 1')
    await asyncio.sleep(1)
    print('Do some actions 2')
    await asyncio.sleep(1)
    print('Do some actions 3')


if __name__ == '__main__':
    loop = asyncio.get_event_loop()
    loop.run_until_complete(main())

    # Let's also cancel all running tasks:
    pending = asyncio.Task.all_tasks()
    for task in pending:
        task.cancel()
        # Now we should await task to execute it's cancellation.
        # Cancelled task raises asyncio.CancelledError that we can suppress:
        with suppress(asyncio.CancelledError):
            loop.run_until_complete(task)

出力:

Do some actions 1
echo
Do some actions 2
echo
Do some actions 3
echo

私は最初のブロックをコピーして通過し、単にそれを私の端で実行しました、そして何らかの理由で私は得ました:4行目の関数定義に構文エラーがあるかのように: "async def async_foo( ): "何か不足していますか?
Gil Allen

3
@GilAllenこの構文はPython 3.5以降でのみ機能します。Python 3.4には古い構文が必要です(docs.python.org/3.4/library/asyncio-task.htmlを参照)。Python 3.3以前はasyncioをまったくサポートしていません。
ミハイルゲラシモフ2016年

スレッド内のタスクをどのように強制終了しますか?…̣いくつかのタスクを作成するスレッドがあり、スレッドがそのstop()メソッドで停止したときに保留中のすべてのタスクを強制終了したいです。
Sardathrion-SEの悪用に対して

@Sardathrionタスクが作成されたスレッドのどこかをタスクが指しているかどうかはわかりませんが、手動で追跡することを妨げるものはありません。たとえば、スレッドで作成されたすべてのタスクをリストに追加し、時間が来たらキャンセルして説明します上記。
ミハイルゲラシモフ2017年

2
「Task.all_tasks()はPython 3.7以降非推奨です。代わりにasyncio.all_tasks()を使用してください」
Alexis

12

簡潔な答えをしてくれたSergeyに感謝します。これは同じの装飾バージョンです。

import asyncio
import time

def fire_and_forget(f):
    def wrapped(*args, **kwargs):
        return asyncio.get_event_loop().run_in_executor(None, f, *args, *kwargs)

    return wrapped

@fire_and_forget
def foo():
    time.sleep(1)
    print("foo() completed")

print("Hello")
foo()
print("I didn't wait for foo()")

生産する

>>> Hello
>>> foo() started
>>> I didn't wait for foo()
>>> foo() completed

注:プレーンスレッドを使用して同じことを行う他の回答を確認してください。


このアプローチを使用した後、毎秒最大5個の小さなFire-and-Forgetタスクを作成した後、かなりの速度低下が発生しました。これを本番環境で長時間実行するタスクに使用しないでください。CPUとメモリを消費します!
ピル

10

これは完全に非同期の実行ではありませんが、おそらくrun_in_executor()が適しています。

def fire_and_forget(task, *args, **kwargs):
    loop = asyncio.get_event_loop()
    if callable(task):
        return loop.run_in_executor(None, task, *args, **kwargs)
    else:    
        raise TypeError('Task must be a callable')

def foo():
    #asynchronous stuff here


fire_and_forget(foo)

3
いい簡潔な答え。executorはデフォルトでを呼び出すことに注意してくださいconcurrent.futures.ThreadPoolExecutor.submit()。スレッドの作成は無料ではないため、ここで触れます。1秒間に1000回の火と忘却は、おそらくスレッド管理に大きな負担をかけるでしょう
Brad Solomon

うん。私はあなたの警告に気を付けず、このアプローチを使用して1秒あたり最大5つの小さな実行タスクを作成した後、大幅な速度低下を経験しました。これを本番環境で長時間実行するタスクに使用しないでください。CPUとメモリを消費します!
ピル

3

何らかの理由で使用できない場合asyncioは、プレーンスレッドを使用した実装を次に示します。私の他の答えとセルゲイの答えもチェックしてください。

import threading

def fire_and_forget(f):
    def wrapped():
        threading.Thread(target=f).start()

    return wrapped

@fire_and_forget
def foo():
    time.sleep(1)
    print("foo() completed")

print("Hello")
foo()
print("I didn't wait for foo()")

このfire_and_forget機能だけが必要で、asyncioからは何も必要ない場合でも、asyncioを使用する方が良いでしょうか?メリットは何ですか?
ピル
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.