5
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Pythonで非同期処理をシンプルに書けない例

Last updated at Posted at 2024-03-16

はじめに

@y_kato_eng (Yuuki Kato) さんの記事「非同期処理をシンプルなPythonコードで説明する」の冒頭に文章で書かれている処理をPythonで書いてみようと思いました。しかし、非同期処理を意図したものが同期処理になってしまい、シンプルに書けなかったので紹介いたします。

作成したい非同期処理

10分かけてご飯を炊いている間に、2分かけて食材を切り、5分かけて部屋を掃除するサンプルコードを書こうと思いました。
そこで、asyncio.sleepを使用して、10秒間スリープするタスクと、2秒スリープした後に5秒スリープするタスクの2つを非同期で実行し、全体で10秒で終わるサンプルコードを作成してみることにしました。実際に10分かけて動作させると待つのが大変なので10秒に短縮しています。

JavaScriptでのサンプルコード

JavaScriptで非同期処理を書いた経験があるので、まずはJavaScriptでのサンプルコードを示します。
JavaScriptではsetTimeout関数とPromiseを組み合わせて非同期sleep関数を作ります。
以下のコードを実行すると全処理が10秒で終わります。

start = Date.now()

function sleep(seconds) {
    return new Promise(resolve => setTimeout(resolve, seconds * 1000))
}

function log(message) {
    console.log(`${Math.trunc((Date.now() - start) / 1000)}秒経過`, message)
}

async function ご飯を炊く() {
    log("ご飯を炊きます")
    await sleep(10)
    log("10分かかってご飯が炊けました")
}

async function 食材を切る() {
    log("食材を切ります")
    await sleep(2)
    log("2分かけて食材を切りました")
}

async function 部屋の掃除をする() {
    log("部屋を掃除します")
    await sleep(5)
    log("5分かけて部屋を掃除しました")
}

async function main() {
    log("タスク開始")
    const 裏タスク = ご飯を炊く()
    await 食材を切る()
    await 部屋の掃除をする()
    await 裏タスク
    log("タスク終了")
}

main()
実行結果
0秒経過 タスク開始
0秒経過 ご飯を炊きます
0秒経過 食材を切ります
2秒経過 2分かけて食材を切りました
2秒経過 部屋を掃除します
7秒経過 5分かけて部屋を掃除しました
10秒経過 10分かかってご飯が炊けました
10秒経過 タスク終了

Pythonに単純移植したサンプルコード

Pythonにそのまま移植すると、全処理に17秒かかってしまいました。

import asyncio
import datetime

start = datetime.datetime.now()

def log(message):
    print(f"{(datetime.datetime.now() - start).seconds}秒経過", message)

async def ご飯を炊く():
    log("ご飯を炊きます")
    await asyncio.sleep(10)
    log("10分かかってご飯が炊けました")

async def 食材を切る():
    log("食材を切ります")
    await asyncio.sleep(2)
    log("2分かけて食材を切りました")

async def 部屋の掃除をする():
    log("部屋を掃除します")
    await asyncio.sleep(5)
    log("5分かけて部屋を掃除しました")

async def main():
    log("タスク開始")
    裏タスク = ご飯を炊く()
    await 食材を切る()
    await 部屋の掃除をする()
    await 裏タスク
    log("タスク終了")

asyncio.run(main())
実行結果
0秒経過 タスク開始
0秒経過 食材を切ります
2秒経過 2分かけて食材を切りました
2秒経過 部屋を掃除します
7秒経過 5分かけて部屋を掃除しました
7秒経過 ご飯を炊きます
17秒経過 10分かかってご飯が炊けました
17秒経過 タスク終了

全処理が10秒で終了するように対処したサンプルコード

pythonでasyncio.sleepを同時に動かすには、asyncio.gatherを使用する必要がありました。
タスクごとに関数を作り、asyncio.gatherの引数に渡します。

import asyncio
import datetime

start = datetime.datetime.now()

def log(message):
    print(f'{(datetime.datetime.now() - start).seconds}秒経過', message)

async def ご飯を炊く():
    log("ご飯を炊きます")
    await asyncio.sleep(10)
    log("10分かかってご飯が炊けました")

async def 食材を切る():
    log("食材を切ります")
    await asyncio.sleep(2)
    log("2分かけて食材を切りました")

async def 部屋の掃除をする():
    log("部屋を掃除します")
    await asyncio.sleep(5)
    log("5分かけて部屋を掃除しました")

async def 裏タスク():
    await ご飯を炊く()

async def 表タスク():
    await 食材を切る()
    await 部屋の掃除をする()

async def main():
    log("タスク開始")
    await asyncio.gather(裏タスク(), 表タスク())
    log("タスク終了")

asyncio.run(main())
実行結果
0秒経過 タスク開始
0秒経過 ご飯を炊きます
0秒経過 食材を切ります
2秒経過 2分かけて食材を切りました
2秒経過 部屋を掃除します
7秒経過 5分かけて部屋を掃除しました
10秒経過 10分かかってご飯が炊けました
10秒経過 タスク終了

全処理が10秒で終了するように対処したサンプルコード(その2)

@tenmyo さんから、create_taskを教えていただきました。
シンプルにはなりましたが、メッセージ出力順序がちょっと違っています。

import asyncio
import datetime

start = datetime.datetime.now()

def log(message):
    print(f"{(datetime.datetime.now() - start).seconds}秒経過", message)

async def ご飯を炊く():
    log("ご飯を炊きます")
    await asyncio.sleep(10)
    log("10分かかってご飯が炊けました")

async def 食材を切る():
    log("食材を切ります")
    await asyncio.sleep(2)
    log("2分かけて食材を切りました")

async def 部屋の掃除をする():
    log("部屋を掃除します")
    await asyncio.sleep(5)
    log("5分かけて部屋を掃除しました")

async def main():
    log("タスク開始")
    裏タスク = asyncio.create_task(ご飯を炊く())
    await 食材を切る()
    await 部屋の掃除をする()
    await 裏タスク
    log("タスク終了")

asyncio.run(main())
実行結果
0秒経過 タスク開始
0秒経過 食材を切ります
0秒経過 ご飯を炊きます
2秒経過 2分かけて食材を切りました
2秒経過 部屋を掃除します
7秒経過 5分かけて部屋を掃除しました
10秒経過 10分かかってご飯が炊けました
10秒経過 タスク終了

さいごに

JavaScriptではタイマースレッドが裏で動いており、setTimerはタイマースレッドに処理依頼するため、シンプルな記述で同時実行できます。
一方、Pythonにはタイマースレッドがないため、async.gatherを使用して複数のasync.sleep処理を同時に実行させる必要がありました。
そのため、タスクごとに処理をまとめた関数を作成する必要があり、シンプルに書けない例になりました。

5
4
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?