※この記事はUdon Advent Calendar 2024 - Adventarの6日目の記事です。
はじめに
こんにちは。Udonです。
今日はawaitとasyncについて書いていこうと思います。
背景
この記事やこの記事で書いたように、最近Pythonを使ってDiscord Botを作っています。
その際、awaitやらasyncといったキーワードが多発しました。参考にした記事でそう書いてあったからとりあえずそれを使っておけばよいのか、と思っていて、しっかりとした理解をしていませんでした。
なので、今回はこれらのキーワードについて詳しく調べてみることにしました。
非同期処理とは
awaitとasyncを理解する前に、「非同期処理」についてわかっておく必要があります。
非同期処理とは、処理の完了を待たずに次の処理を進めておくということです。
Pythonは基本的に上から順に処理を行います。なので、プログラムの上のほうに書いてある処理が重いと、たとえ下のほうの処理が軽かったとしても、上のほうの処理が終わるまで下のほうの処理が進まないということになります。
非同期処理を使うことで、処理の完了を待たずに次の処理を進めることができます。これにより、処理の効率を上げることができるわけです。
awaitとasync
Pythonプログラムにおいて、非同期処理を行う関数を定義するときには、async
というキーワードを関数の前につけます。
async def print_hello():
print('Hello')
このように書くことで、print_hello
関数は非同期処理として扱われるようになります。
また、非同期処理の完了を待つときには、await
というキーワードを使います。
async def main():
await print_hello()
print('World')
このように書くことで、print_hello
関数の処理が完了するまでWorld
の表示を待つことができます。
加えて、非同期処理を実装する際には、asyncio
モジュールを使うことが一般的です。
sleep
関数を処理に組み込みたい場合は、以下のように書く必要があります。
import asyncio
async def main():
print('Hello')
await asyncio.sleep(1)
print('World')
このコードを実行すると、Hello
の表示後1秒待ってからWorld
と表示されます。
では、実際に非同期処理が行われている例を確認しましょう。
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
を使ったらどうなるでしょうか。
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
を使い、待たなくてもいい場合は使わない、という使い分けができるわけです。
おわりに
今回は非同期処理について調べてみました。
もしかしたら理解が間違っていたりするかもしれないので、もし間違いがあれば教えていただけると幸いです。自分で気づいた場合は記事を更新しようと思います。
後に示す参考文献も併せて読んでいただけると幸いです。
それでは、また明日の記事でお会いしましょう。