Day 57:非同期処理の基礎 ― 「待っている時間」をうまく使う考え方
Day 57では、Web・APIの世界でよく登場する 非同期処理 の基本概念を整理していきます。
キーワードはこの4つです。
- 同期処理
- 非同期処理
asyncawait
今日は「速いコードを書く」というよりも、 「待っている時間をどう扱うか」 をイメージで理解する回だと思って読んでみてください。
同期処理とは:1列に並んで、順番に終わるのを待つ世界
同期処理のイメージ
まずは「同期処理」からです。
同期処理は、ざっくり言うと、
「1つの処理が終わるまで、次の処理は待っている」
世界です。
例えば、次のような流れを考えてみます。
- API A にリクエストを送る
- 結果が返ってくるまで待つ
- API B にリクエストを送る
- 結果が返ってくるまで待つ
このとき、Aの結果を待っている間、 Bのリクエストはまだ送られません。
同期処理のコード例
Pythonで「同期的に」処理を書くと、こんな感じになります。
# day57_sync_example.py
import time
def slow_task(name: str, seconds: int) -> None:
"""
時間のかかる処理を模した関数です。
time.sleep() で「待ち時間」を作っています。
"""
print(f"[{name}] {seconds} 秒かかる処理を開始します。")
time.sleep(seconds) # 指定秒数だけ「何もしないで待つ」
print(f"[{name}] 処理が完了しました。")
def main():
print("同期処理の例を開始します。")
# 1つ目の処理
slow_task("task1", 2)
# 2つ目の処理(1つ目が終わってから始まります)
slow_task("task2", 3)
print("同期処理の例を終了します。")
if __name__ == "__main__":
main()
Pythonこのコードでは、
task1が2秒かかる- その後に
task2が3秒かかる
ので、合計で約5秒かかります。
「1つずつ順番に処理する」 これが同期処理の基本的なイメージです。
非同期処理とは:「待っている間に、別のことも進める」世界
非同期処理のイメージ
非同期処理は、ざっくり言うと、
「ある処理が『待ち』の状態になったら、その間に別の処理も進める」
世界です。
例えば、次のような流れを考えてみます。
- API A にリクエストを送る
- 結果が返ってくるまでの間に、API B にもリクエストを送る
- AとBの結果がそれぞれ返ってきたら、順番に処理する
このとき、「待っている時間」をうまく使うことで、 全体の時間を短くできる 可能性があります。
非同期処理が役立つ場面
非同期処理が特に役立つのは、こんな場面です。
- Web APIに複数リクエストを送る
- ファイルの読み書きやネットワーク通信など「待ち時間」が多い処理
- ブラウザ自動化やWebスクレイピングで、複数ページを扱うとき
CPUが重い計算をしているときではなく、 「待っている時間」が多い処理 に向いている、というのがポイントです。
async と await:非同期処理を書くための「合図」
async は「非同期関数ですよ」という宣言
Pythonでは、非同期処理を書くときに async def という形で関数を定義します。
async def my_async_function():
...
Pythonこの async は、
「この関数は、非同期的に動く関数ですよ」
という宣言です。
普通の def と違って、 この関数の中では await を使うことができます。
await は「ここで一旦、他の処理に譲ります」という合図
await は、非同期処理の中で使うキーワードで、
「ここで一旦、処理を止めて、他の処理に進んでもいいですよ」
という合図になります。
例えば、
- 非同期版の
sleep - 非同期版の HTTPリクエスト
- 非同期版のファイル操作
などを呼び出すときに await を付けます。
非同期処理の基本例:async と await を使って「待ち時間」を並列にする
asyncio を使ったシンプルな例
Python標準ライブラリの asyncio を使って、 同期処理と非同期処理の違いを体感できる例を書いてみます。
# day57_async_example.py
import asyncio
async def async_slow_task(name: str, seconds: int) -> None:
"""
非同期版の「時間のかかる処理」を模した関数です。
asyncio.sleep() を await することで、「待っている間に他の処理も進められる」ようになります。
"""
print(f"[{name}] {seconds} 秒かかる処理を開始します。")
await asyncio.sleep(seconds) # 非同期の「待ち」
print(f"[{name}] 処理が完了しました。")
async def main_async():
print("非同期処理の例を開始します。")
# 2つのタスクを「同時に」走らせるイメージでスケジュールします。
task1 = asyncio.create_task(async_slow_task("task1", 2))
task2 = asyncio.create_task(async_slow_task("task2", 3))
# 2つのタスクが両方終わるまで待ちます。
await task1
await task2
print("非同期処理の例を終了します。")
if __name__ == "__main__":
# 非同期関数を実行するために、asyncio.run() を使います。
asyncio.run(main_async())
Pythonこのコードでは、
task1が2秒task2が3秒
かかりますが、 「待ち時間を重ねて使う」 ので、 全体の時間は約3秒になります。
同期版の例では約5秒かかっていたので、 「待っている時間をうまく使うと、全体が短くなる」 というイメージがつかみやすくなります。
同期 vs 非同期を並べて見てみる
同期版と非同期版の違いを言葉で整理する
ここまでの例を、言葉で並べてみます。
- 同期版
time.sleep()で待っている間、他の処理は進まない- 2秒+3秒で、合計約5秒かかる
- 非同期版
await asyncio.sleep()で待っている間、他のタスクも進められる- 2秒と3秒が重なって進むので、合計約3秒で終わる
どちらも「正しく動く」コードですが、 「待ち時間の扱い方」が違う というのがポイントです。
async / await を使うときの基本ルール
ルール1:await は async 関数の中でしか使えない
await は、必ず async def で定義された関数の中で使います。
async def example():
await asyncio.sleep(1) # OK
Python普通の def の中で await を書くと、 構文エラーになります。
ルール2:非同期関数を実行するときは「イベントループ」が必要
async def で定義した関数は、 そのままでは「ただの非同期関数のオブジェクト」で、 すぐには実行されません。
実行するときは、
asyncio.run()を使う- 既存のイベントループの中で
awaitする
といった形になります。
import asyncio
async def hello():
print("Hello from async function!")
async def main():
await hello()
if __name__ == "__main__":
asyncio.run(main())
Pythonここでは、
main()が非同期関数main()の中でawait hello()を呼び出すasyncio.run(main())で、イベントループを作ってmain()を実行する
という流れになっています。
非同期処理がWeb・APIで効いてくる場面
複数のAPIを「まとめて」呼びたいとき
Web・APIの世界では、 次のような場面で非同期処理が効いてきます。
- ユーザー情報API
- 商品情報API
- 在庫情報API
など、複数のAPIを呼び出して、 結果を組み合わせて画面を作るようなケースです。
同期的に書くと、
- ユーザー情報APIを呼ぶ → 待つ
- 商品情報APIを呼ぶ → 待つ
- 在庫情報APIを呼ぶ → 待つ
となりますが、 非同期的に書くと、
- 3つのAPIをほぼ同時に呼ぶ
- それぞれの結果が返ってくるのを待つ
という形になり、 全体の待ち時間を短くできる 可能性があります。
Day 57ミニテンプレート:同期 vs 非同期を比べる練習用コード
最後に、Day 57の内容をコンパクトにまとめた 「同期 vs 非同期 比較テンプレート」 を載せておきます。
# day57_sync_vs_async_template.py
import time
import asyncio
def slow_task_sync(name: str, seconds: int) -> None:
"""同期版の「時間のかかる処理」です。"""
print(f"[SYNC {name}] {seconds} 秒かかる処理を開始します。")
time.sleep(seconds)
print(f"[SYNC {name}] 処理が完了しました。")
async def slow_task_async(name: str, seconds: int) -> None:
"""非同期版の「時間のかかる処理」です。"""
print(f"[ASYNC {name}] {seconds} 秒かかる処理を開始します。")
await asyncio.sleep(seconds)
print(f"[ASYNC {name}] 処理が完了しました。")
def run_sync():
print("=== 同期処理のデモを開始します ===")
start = time.time()
slow_task_sync("task1", 2)
slow_task_sync("task2", 3)
end = time.time()
print(f"同期処理の合計時間: {end - start:.2f} 秒")
print("=== 同期処理のデモを終了します ===\n")
async def run_async():
print("=== 非同期処理のデモを開始します ===")
start = time.time()
# 2つのタスクを同時にスケジュールします。
task1 = asyncio.create_task(slow_task_async("task1", 2))
task2 = asyncio.create_task(slow_task_async("task2", 3))
# 両方のタスクが終わるまで待ちます。
await task1
await task2
end = time.time()
print(f"非同期処理の合計時間: {end - start:.2f} 秒")
print("=== 非同期処理のデモを終了します ===")
def main():
# 同期版のデモ
run_sync()
# 非同期版のデモ
asyncio.run(run_async())
if __name__ == "__main__":
main()
Pythonこのテンプレートを実行すると、
- 同期版は約5秒
- 非同期版は約3秒
という違いが、実際の数字として確認できるはずです。
Day 57のまとめ
Day 57では、非同期処理の基礎として、
- 同期処理は「1つの処理が終わるまで次を待つ」世界であること
- 非同期処理は「待っている間に別の処理も進める」世界であること
asyncは「非同期関数ですよ」という宣言で、awaitは「ここで一旦他に譲ります」という合図であることasyncioを使うことで、async/awaitを使った非同期処理を簡単に書けること- Web・APIの世界では、複数のリクエストをまとめて処理するときに非同期処理が効いてくること
を、同期版と非同期版のコードを並べながら整理しました。
ここで概念がつかめていると、 次のステップで登場する「非同期HTTPクライアント」や「非同期Webフレームワーク」のコードも、 ぐっと読みやすく感じられるようになっていきます。
