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

## はじめに

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

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

## JavaScriptでのサンプルコード

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

```js
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秒かかってしまいました。

```py
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`の引数に渡します。

```py
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秒で終了するように対処したサンプルコード（その２）

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

```py
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処理を同時に実行させる必要がありました。
そのため、タスクごとに処理をまとめた関数を作成する必要があり、シンプルに書けない例になりました。