Python3.7以降から使用可能なasyncioモジュールを使用することで非同期処理IOが実装できる。
しくみ
非同期IO処理とは、簡単に言えば「あるIO処理の終了を待たずに他の処理をする」こと。今までであれば時間のかかる処理を呼び出せば、その処理が終了するまで処理は止まって(ブロックされて)待たなければならなかった。これが同期処理。非同期処理は処理の終了を待たずに、その間に他の処理をできる。(ノンブロックキング)
threadingやmultiprocessでも似たように、処理の終了を待たずに他の処理をすることができるが、asyncioによる非同期IO処理はそれよりももっと軽量に動作し、実際には並列して処理が動くことはなく1スレッドで動いている処理は1つだけになる。
「同時に処理が動かない」のに「処理の終了を待たずに他の処理ができる」のは奇妙に思うかもしれない。IO処理というのはCPUを使わず処理が終わるのを待っている時間がある。例えばファイルの読み込みをしている間は、CPUを使わずにファイルのデータが読み込まれるのを待っている。この時間はCPU的には十分に長い時間なのでこの時間に他のことが処理できる。
非同期IO処理は「空き時間にこの処理をして!終わったら教えて!」という細かい処理をどんどん登録して、「空いた!」「じゃあこれやって!」という小さな処理単位をイベントループというループの中でどんどん処理する。処理自体は同時に1つしかこなせないが、空き時間を有効活用していることになる。ちょうど料理中に電子レンジをONにして、チンと完了するまでに他の作業をするのに似た感じ。
注意点
処理単位のなかで、長く時間のかかるブロッキング処理をしてしまうと、従来の同期処理と同じことになってしまうため避けなければならない。
例えばtime.sleepはスリープする処理だが、これは同期処理のsleepであり、非同期IOにはasyncio.sleepという非同期用のsleepがある。間違って非同期処理の中で、time.sleepを使うと非同期処理は他の処理をプログラムはそこでブロックして他のことを処理できなくなる。
このため、今まで同期処理でやっていたことと違うコードを書くことになるので注意する必要がある。
サンプルコード
import asyncio
import time
async def say_after(delay, what):
await asyncio.sleep(delay)
print(f"{time.strftime('%X')} - {what}")
async def main():
print(f"{time.strftime('%X')} - start")
c1 = say_after(1, 'hello')
c2 = say_after(2, 'world')
await asyncio.gather(c1, c2) #2つの処理の終了を待つ
print(f"{time.strftime('%X')} - finish")
# 非同期IOのエントリポイント
asyncio.run(main())
実行結果
16:10:28 - start
16:10:29 - hello
16:10:30 - world
16:10:30 - finish
1秒後にhelloと出力する処理、2秒後にworldと出力する処理を実行する。say_after関数は完了を待たずにすぐに返ってくる。asyncが設定された関数はそのような処理になる。asyncが設定された関数の処理を待つ場合にはawaitを使う。待つと書いたが実際にはこの間に他のasync処理を実行して、終わった後に戻ってくるような動きをする。
この結果、2つのsay_afterの処理はそれぞれ1秒、2秒まってから処理が行われるので全体としては2秒ですべてが終了する。
awaitが使えるのはasync関数の中だけになる。これが重要でasyncで非同期処理を使いたいと思ったときにはそこがasyncでなければならない。それを呼び出す関数もasyncでなければならない。どんどん遡ったトップレベルのエントリポイントで、asyncio.runなどから起動されていなければならない。
await.sleepをtime.sleepにすると、sleepでブロックされてその他の処理ができなくなってしまう。
import asyncio
import time
async def say_after(delay, what):
# await asyncio.sleep(delay)
time.sleep(delay) # 非同期ioではないsleep
print(f"{time.strftime('%X')} - {what}")
async def main():
print(f"{time.strftime('%X')} - start")
c1 = say_after(1, 'hello') #time.sleepがブロックするのでc1の取得に1秒かかる
c2 = say_after(2, 'world')
await asyncio.gather(c1, c2) #2つの処理の終了を待つ
print(f"{time.strftime('%X')} - finish")
# 非同期IOのエントリポイント
asyncio.run(main())
実行結果
11:05:37 - start
11:05:38 - hello
11:05:40 - world
11:05:40 - finish
実行結果からわかるように、startからhelloまで1秒かかり、helloからworldに2秒かかっている。