at backyard

Color my life with the chaos of trouble.

Pythonのasyncioで投げっぱなしの処理(fire and forget)の実装サンプル

普段JavaScriptでコードを書いていると、投げっぱなしの処理を書くというのはとても簡単だ。
(ちなみに投げっぱなしの処理は英語では fire and forget というらしい)

今回JavaScriptで書けるようなノンブロッキングな処理について、Pythonのasyncioを用いてシンプルに書くにはどうすれば良いのか調べていたのでこちらに内容をまとめる。
なお、このポストではノンブロッキングとか非同期などの言葉の解説や、それに付随するあれこれについての解説は含まない。

完全に備忘録としての形をなしている。

目次

JavaScript(Node.js)での投げっぱなし処理のサンプル

JavaScriptではその処理系の特性上、ノンブロッキングな処理は簡単に書くことができる。

サンプル、の前に環境を下に記載する.

$ node --version
v14.17.1

例えば下記のようなサンプルを書いてみる。

const sleep = msec => new Promise(resolve => setTimeout(resolve, msec));

async function displayLog(num) {
  await sleep(1000)
  console.log("DISPLAY LOG: ", num);
}

(async () => {
  console.log('START');

  displayLog(1) 
  displayLog(2) 
  displayLog(3)

  console.log('FINISH');
})();

この処理を実行した場合、下記のような実行結果となる。
(1,2,3 はほぼ同時に出力される)

START
FINISH
DISPLAY LOG:  1
DISPLAY LOG:  2
DISPLAY LOG:  3

displayLog 関数の返り値を待たずに次の処理に移っているため、以上のような結果となる。

Pythonのasyncioを用いた、投げっぱなし処理のサンプル

では、次にPythonのasyncioを用いた投げっぱなし処理のサンプルを記載する。

その前にPythonの環境を下に記載する。

$ python --version
Python 3.10.1

上の通り3.10を利用している。
少し古い記事だと書き方が異なっているポストも見受けられたが、3.10だとこの書き方がシンプルに書ける投げっぱなし(fire and forget)な処理になるかと思う。

import asyncio
from time import sleep


def display_log(num):
    sleep(1)
    print("DISPLAY LOG: " + str(num))


if __name__ == "__main__":
    print("START")

    loop = asyncio.new_event_loop()
    asyncio.set_event_loop(loop)

    loop.run_in_executor(None, display_log, 1)
    loop.run_in_executor(None, display_log, 2)
    loop.run_in_executor(None, display_log, 3)

    print("FINISH")

このコードを実行すると、下記のようになる。
(1,2,3はほぼ同時に出力されるし、これらの順番は前後する)

START
FINISH
DISPLAY LOG: 1
DISPLAY LOG: 2
DISPLAY LOG: 3

DeprecationWarning: There is no current event loop

投げっぱなしの処理について色々調べている間に DeprecationWarning: There is no current event loop という警告に出くわした。

これは下記のコードのように書いた場合に出ていた。

import asyncio
from time import sleep


def display_log(num):
    sleep(1)
    print("DISPLAY LOG: " + str(num))


if __name__ == "__main__":
    print("START")

    loop = asyncio.get_event_loop()

    loop.run_in_executor(None, display_log, 1)
    loop.run_in_executor(None, display_log, 2)
    loop.run_in_executor(None, display_log, 3)

    print("FINISH")

get_event_loopはこの書き方では非推奨となっているようなので、最初に書いたサンプルを使ったほうが良さそう。
なお、ここらへんの詳細についてはPythonの公式ドキュメントを参照したほうが良い。
(非推奨となる詳細な内容までは踏み込んで調べていない)

Pythonのthreadingを使って投げっぱなしの処理を書く

ちなみに今回 asyncio を用いた投げっぱなし処理について調べていたが、 threading を使っても投げっぱなし処理は書くことができる。
threadingを用いた処理は書いたことがあったが、asyncioのほうが新しいし、こちらを使ってみたいというのが今回調べていた動機の一つとなる。

import threading
from time import sleep


def display_log(num):
    def thread_display_log():
        sleep(1)
        print("DISPLAY LOG: " + str(num))
    thread = threading.Thread(target=thread_display_log)
    thread.start()


if __name__ == "__main__":
    print("START")

    display_log(1)
    display_log(2)
    display_log(3)

    print("FINISH")

これで実行すると、下記のような結果になる。
(こちらも 1,2,3はほぼ同時に出力されるし、これらの順番は前後する)

START
FINISH
DISPLAY LOG: 2
DISPLAY LOG: 1
DISPLAY LOG: 3

調べた内容については以上となる。

デコレーターを用いた投げっぱなし処理の実装

色々と調べている際にデコレーターを用いた実装について触れられているポストも目にした。
そこでせっかくなので今回のコードもデコレーターを用いた形に直してみることにした。

import asyncio
from time import sleep


def fire_and_forget(func):
    def wrapper(*args, **kwargs):
        loop = asyncio.new_event_loop()
        asyncio.set_event_loop(loop)
        return loop.run_in_executor(None, func, *args, *kwargs)
    return wrapper


@fire_and_forget
def display_log(num):
    sleep(1)
    print("DISPLAY LOG: " + str(num))


if __name__ == "__main__":
    print("START")

    display_log(1)
    display_log(2)
    display_log(3)

    print("FINISH")

こちらのコードを実行した場合の結果は下記。
(こちらも 1,2,3はほぼ同時に出力されるし、これらの順番は前後する)

START
FINISH
DISPLAY LOG: 1
DISPLAY LOG: 3
DISPLAY LOG: 2

デコレーターを用いた場合、関数呼び出し時などにそのまま関数呼び出しを書けるので、呼び出し箇所などはスッキリする。