awaitとasyncを理解しようとしてみる

<!-- awaitとasyncを理解しようとしてみる -->

※この記事は[Udon Advent Calendar 2024 - Adventar](https://adventar.org/calendars/10764)の6日目の記事です。

## はじめに

こんにちは。Udonです。

今日はawaitとasyncについて書いていこうと思います。

## 背景

[この記事](https://qiita.com/Q_Udon/items/35cf098bdec0a6a2204a)や[この記事](https://qiita.com/Q_Udon/items/7ae12f7280c60f8618b3)で書いたように、最近Pythonを使ってDiscord Botを作っています。

その際、awaitやらasyncといったキーワードが多発しました。参考にした記事でそう書いてあったからとりあえずそれを使っておけばよいのか、と思っていて、しっかりとした理解をしていませんでした。

なので、今回はこれらのキーワードについて詳しく調べてみることにしました。

## 非同期処理とは

awaitとasyncを理解する前に、「非同期処理」についてわかっておく必要があります。

非同期処理とは、処理の完了を待たずに次の処理を進めておくということです。

Pythonは基本的に上から順に処理を行います。なので、プログラムの上のほうに書いてある処理が重いと、たとえ下のほうの処理が軽かったとしても、上のほうの処理が終わるまで下のほうの処理が進まないということになります。

非同期処理を使うことで、処理の完了を待たずに次の処理を進めることができます。これにより、処理の効率を上げることができるわけです。

## awaitとasync

Pythonプログラムにおいて、非同期処理を行う関数を定義するときには、``async``というキーワードを関数の前につけます。

```python
async def print_hello():
    print('Hello')
```

このように書くことで、``print_hello``関数は非同期処理として扱われるようになります。

また、非同期処理の完了を待つときには、``await``というキーワードを使います。

```python
async def main():
    await print_hello()
    print('World')
```

このように書くことで、``print_hello``関数の処理が完了するまで``World``の表示を待つことができます。

加えて、非同期処理を実装する際には、``asyncio``モジュールを使うことが一般的です。

``sleep``関数を処理に組み込みたい場合は、以下のように書く必要があります。

```python
import asyncio

async def main():
    print('Hello')
    await asyncio.sleep(1)
    print('World')
```

このコードを実行すると、``Hello``の表示後1秒待ってから``World``と表示されます。

では、実際に非同期処理が行われている例を確認しましょう。

```python
import asyncio

async def hello():
    print('Hello')
    await asyncio.sleep(10)
    print('World')

async def heavy():
    for _ in range(5):
        await asyncio.sleep(1)
    print('Heavy')

async def main():
    await asyncio.gather(hello(), heavy())

asyncio.run(main())
```

このコードを実行すると以下のようになります。

```
Hello
Heavy
World
```

1行目の5秒後に2行目、そのまた5秒後に3行目が表示されます。全体としては10秒、つまり長いほうの処理時間かかっているわけです。

では、``await``を使ったらどうなるでしょうか。

```python
import asyncio

async def hello():
    print('Hello')
    await asyncio.sleep(10)
    print('World')

async def heavy():
    for _ in range(5):
        await asyncio.sleep(1)
    print('Heavy')

async def main():
    await hello()
    await heavy()

asyncio.run(main())
```

このコードを実行すると以下のようになります。

```
Hello
World
Heavy
```

1行目の10秒後に2行目、そのまた5秒後に3行目が表示されます。つまり全体として15秒かかっているわけです。

これが並列処理と直列処理の違いとなるわけです。``await``を使うことで、非同期処理を行う関数に直列処理を行わせるようなことができる、ということを覚えておくのが良いと思います。

## Botの実行において

Discord Botの場合、ユーザからのメッセージをトリガーとしてBotが反応するような機能を実装する必要があります。そんなときに非同期処理が必須となるわけです。

ここでは詳細は割愛しますが、Botの起動時やメッセージの受信時などのイベントが発生したときに実行されるものは「イベントハンドラ」といい、非同期処理となっています。そのイベントハンドラが呼び出す関数もまた非同期処理にしなければならないというわけです。なので、Discord Botを司るPythonプログラムで定義する関数は、基本的には非同期処理として書かなければなりません。

例えば、Botがメッセージを受信したとき、何か返信メッセージを生成してからそれを返信したいとしましょう。この場合、メッセージを受信したときに返信メッセージを生成する関数を非同期処理として書いておき、その関数を呼び出すときに``await``を使うことで、返信メッセージが生成されるまで待つことができるわけです。``await``を使わないと、返信メッセージが生成される前に返信が送信されてしまうかもしれません。

前の処理を待ったほうがいい場合は``await``を使い、待たなくてもいい場合は使わない、という使い分けができるわけです。

## おわりに

今回は非同期処理について調べてみました。

もしかしたら理解が間違っていたりするかもしれないので、もし間違いがあれば教えていただけると幸いです。自分で気づいた場合は記事を更新しようと思います。

後に示す参考文献も併せて読んでいただけると幸いです。

それでは、また明日の記事でお会いしましょう。

## 参考文献

- [Pythonのasync/awaitを理解したい【asyncio】](https://zenn.dev/iharuoru/articles/45dedf1a1b8352)
- [Pythonで非同期処理をするときのasyncioの使い方 | ABC DX Tech Blog](https://tech.asahi.co.jp/posts/20240328-7eb3)
- [Pythonの非同期プログラミングを完全理解 #非同期処理 - Qiita](https://qiita.com/kaitolucifer/items/3476158ba5bd8751e022)
- [非同期処理をシンプルなPythonコードで説明する #Python - Qiita](https://qiita.com/y_kato_eng/items/ca0de5cf1224c807e7e5)