1. 非同期処理とは
非同期処理を一言で表すと、
「待ち時間のある処理をしている間に、別の処理を進められる仕組み」 のことです。
例えば、以下のような場合が非同期処理に適しています。
- webAPIの応答を待つ
- ファイルの読み込み
- データベースへのアクセス
- タイマーで待つ(sleep)
これらの処理は待っている間に別の処理をできたほうが効率的です。
この待ち時間の間に別の処理を進められるようにするのが非同期処理です。
2. 同期処理と非同期処理を確認する
実際にPythonコードを用いて同期処理と非同期処理を比較していきます。
2.1. 同期処理と非同期処理のコード
コードの大きな流れとしては、以下のようになっております。
- 同期処理、非同期処理に共通する部分のコード
- 同期処理についてのコード
- 非同期処理についてのコード
from datetime import datetime
import time
import asyncio
# 共通
def elapsed_time(start_time):
#経過時間を求める関数
return (f'{(datetime.now() - start_time).seconds}秒経過')
# 同期処理
start_time_sync = datetime.now() # 経過時間を求めるための開始時刻(同期処理)
def sync_task(name, processing_time):
# 同期処理の処理内容を定義(processing_time秒停止する)
print(f'{name}を開始')
time.sleep(processing_time)
print(f'{name}が終了。{elapsed_time(start_time_sync)}')
def sync_tasks_run():
# 複数の同期処理を実行する
sync_task('同期タスクA', 2)
sync_task('同期タスクB', 3)
sync_task('同期タスクC', 5)
sync_tasks_run() # 同期処理実行関数を呼び出して実行
# 非同期処理
start_time_async = datetime.now() # 経過時間を求めるための開始時刻(非同期処理)
async def async_task(name, processing_time):
# 非同期処理の処理内容を定義(processing_time秒停止する)
print(f'{name}を開始')
await asyncio.sleep(processing_time)
print(f'{name}が終了。{elapsed_time(start_time_async)}')
async def async_tasks_run():
# 複数の非同期処理を実行する
await asyncio.gather(
async_task('非同期タスクA', 2),
async_task('非同期タスクB', 3),
async_task('非同期タスクC', 5)
)
asyncio.run(async_tasks_run()) # 非同期処理実行関数を呼び出して実行
上から順に4つに分けてコードを見ていきます。
from datetime import datetime
import time
import asyncio
1行目は標準ライブラリのdatetimeモジュールです。処理の経過時間を求めるために使用します。
2行目のtimeモジュールは同期処理で使用しています。(処理のsleep)
3行目のasyncioは非同期処理のための標準ライブラリです。
# 共通
def elapsed_time(start_time):
#経過時間を求める関数
return (f'{(datetime.now() - start_time).seconds}秒経過')
ここでは同期処理と非同期処理のどちらでも使用する経過時間を求める関数を定義しています。
# 同期処理
start_time_sync = datetime.now() # 経過時間を求めるための開始時刻(同期処理)
def sync_task(name, processing_time):
# 同期処理の処理内容を定義(processing_time秒停止する)
print(f'{name}を開始')
time.sleep(processing_time)
print(f'{name}が終了。{elapsed_time(start_time_sync)}')
def sync_tasks_run():
# 複数の同期処理を実行する
sync_task('同期タスクA', 2)
sync_task('同期タスクB', 3)
sync_task('同期タスクC', 5)
sync_tasks_run() # 同期処理実行関数を呼び出して実行
ここでは同期処理の内容を記載しています。
start_time_syncは経過時間を求める関数elapsed_timeに渡す値で、基準となる処理の開始時刻として定義します。
sync_taskではprocessing_time秒だけ停止し、開始時刻から何秒経過したかを出力します。
sync_tasks_runでは複数のタスクをまとめて定義しています。このまとめたタスクを呼び出して実行しています。
# 非同期処理
start_time_async = datetime.now() # 経過時間を求めるための開始時刻(非同期処理)
async def async_task(name, processing_time):
# 非同期処理の処理内容を定義(processing_time秒停止する)
print(f'{name}を開始')
await asyncio.sleep(processing_time)
print(f'{name}が終了。{elapsed_time(start_time_async)}')
async def async_tasks_run():
# 複数の非同期処理を実行する
await asyncio.gather(
async_task('非同期タスクA', 2),
async_task('非同期タスクB', 3),
async_task('非同期タスクC', 5)
)
asyncio.run(async_tasks_run()) # 非同期処理実行関数を呼び出して実行
ここでは非同期処理の内容を記載しています。
実施している内容は同期処理で記載した内容と同様のことを非同期処理で記載したものになっています。
asyncやawait asyncio.gatherなどは、このあと確認していきます。
2.2. 実行結果
上記のコードを実行した結果です。
同期タスクAを開始
同期タスクAが終了。2秒経過
同期タスクBを開始
同期タスクBが終了。5秒経過
同期タスクCを開始
同期タスクCが終了。10秒経過
非同期タスクAを開始
非同期タスクBを開始
非同期タスクCを開始
非同期タスクAが終了。2秒経過
非同期タスクBが終了。3秒経過
非同期タスクCが終了。5秒経過
同期実行の方は、順番に実行されているため、タスクAの終了時の経過時間は2秒、タスクB終了時はそこから3秒経過し、開始時刻から5秒経過しています。最後のタスクC終了時にはさらに5秒経過して開始時刻からは10秒経過で終了しています。
一方で、非同期処理の方はタスクA,B,Cの開始の出力が一気に出力されています。
そして最後のタスクCが終了した際の開始時刻からの経過時間は5秒となっています。
非同期処理の方はそれぞれのタスクが終了するのを待たずに開始され、実行が完了していることが確認できます。
2.3. 処理が実行されるイメージ

このように同期処理では、前のタスクが終了するまで、次のタスクは実行されませんが、非同期実行では待ち時間に他の処理を実施することができます。
ここで注意しなくてはならないのは、この非同期処理は並列実行ではなく、並行実行ということです。
2.4. 並行処理と並列処理
2.4.1. 並行処理について(非同期、asyncのイメージ)
例えばあなたは1人で料理をしています。
- お湯を沸かす(5分)
- 野菜を切る(2分)
- 電子レンジで温める(3分)
お湯を沸かしている 5分間、何もしないで待つのは効率が悪いですよね。
そこで…
- お湯が沸くまでの間に野菜を切る
- 切り終わったら、お湯をチェックして
- 電子レンジも動かして
- また野菜に戻る
このように待ち時間に別の作業を切り替えて進めるのが並行処理です。
同時に動いているように見えるけど、実際にはひとつずつ進めているだけです。
2.4.2. 並列処理について(マルチプロセス/マルチスレッドのイメージ)
今度は3人で料理します。
- Aさん:お湯を沸かす
- Bさん:野菜を切る
- Cさん:電子レンジ担当
全員が同時に作業しているので、
本当に「同時に処理」されています。これが並列処理です。
つまり非同期処理は待ち時間を有効活用する仕組みであり、CPU を2つ同時に使うわけではありません。
同時に進んでいるように見せていますが、実際は1つずつ切り替えて実行しているだけということです。
3. 非同期処理で使用される仕組み
次に非同期処理で使用されていたasyncやawait asyncio.gatherなどについて確認していきます。
3.1. async
asyncは非同期関数をつくる宣言
async def hello():
print("Hello!")
普通の関数(def)とは違い、
中でawaitを使えるようになるのが一番大きな特徴です。
3.2. await
awaitは「この処理が終わるまで待つけど、その間 CPU は他の処理に使ってOK」という意味です。
awaitはasync関数の中でだけ使用可能であり、通常の関数(def)で使用することはできません。
async def hello():
await asyncio.sleep(1) # 1秒待っているが、その間ほかの処理が進む
print("Hello!")
3.3. 非同期関数の呼び方
間違った呼び出し
async def hello():
await asyncio.sleep(1) # 1秒待っているが、その間ほかの処理が進む
print("Hello!")
hello() #このように呼ぶことはできない。
async関数は コルーチンという特殊なオブジェクトを返すため、
実行には「イベントループ」が必要です。
実行するときはasyncio.run()を使います。
正しい呼び出し
import asyncio
async def hello():
await asyncio.sleep(1) # 1秒待っているが、その間ほかの処理が進む
print("Hello!")
asyncio.run(hello())
ここでコルーチン、イベントループという言葉が出てきたので確認していきます。
3.4. コルーチン
一言でいうと
途中で一時停止・再開できる関数です。
プログラムの実行中に一時停止し、別のタスクの実行を許可することで、あとから処理の実行を再開することができます。
普通の関数は呼んだら最後までやり切りますが、コルーチンは
→ 自分で「ここで一旦止まるよ」と言える(await)
→ 待ち時間の間に CPU を他の処理に譲れる
async defで定義するとコルーチン関数になります。
呼ぶと「コルーチンオブジェクト」という“実行待ちのタスク”が返ります。
3.5. イベントループ
一言でいうと
コルーチンを管理し、順番に動かすためのスケジューラです。
イベントループの役割は以下です。
- コルーチンの開始
- await に来たらそのコルーチンを止める
- 空いた時間で別のコルーチンを走らせる
- I/O が終わったらそのコルーチンを再開
- すべてのタスクが終わるまで管理し続ける
このイベントループを作成、実行しているのが先程のasyncio.run()です。
3.6. asyncio.gather
asyncio.gatherは 複数の非同期処理(コルーチン)を同時に実行し、その結果をまとめて返す関数です。
渡されたコルーチンがすべて終了するのを待つというのが特徴になります。
非同期関数をまとめて同時に実行し、その結果をすぐ使いたいという場面で使用します。
では、そうでない場合はどのような書き方があるのかというと次に紹介するasyncio.create_taskがあります。
3.7. asyncio.create_task
asyncio.create_taskはコルーチンを「タスクとして起動」するが、すぐには待たない関数です。
はじめに紹介した例を用いて記載します。
from datetime import datetime
import asyncio
# 共通
def elapsed_time(start_time):
#経過時間を求める関数
return (f'{(datetime.now() - start_time).seconds}秒経過')
# 非同期処理
start_time_async = datetime.now() # 経過時間を求めるための開始時刻(非同期処理)
async def async_task(name, processing_time):
# 非同期処理の処理内容を定義(processing_time秒停止する)
print(f'{name}を開始')
await asyncio.sleep(processing_time)
print(f'{name}が終了。{elapsed_time(start_time_async)}')
async def async_tasks_run():
# 複数の非同期処理を実行する
- await asyncio.gather(
- async_task('非同期タスクA', 2),
- async_task('非同期タスクB', 3),
- async_task('非同期タスクC', 5)
- )
+ task1 = asyncio.create_task(
+ async_task('非同期タスクA', 2)
+ )
+ task2 = asyncio.create_task(
+ async_task('非同期タスクB', 3)
+ )
+ task3 = asyncio.create_task(
+ async_task('非同期タスクC', 5)
+ )
+ print('ここはすぐに出力される')
+ await task1 #好きなタイミングで待つことができる
+ await task2 #好きなタイミングで待つことができる
+ await task3 #好きなタイミングで待つことができる
asyncio.run(async_tasks_run()) # 非同期処理実行関数を呼び出して実行
asyncio.gatherの処理を削除し、asyncio.create_taskを追加します。
先程との違いはasyncio.gatherではすぐに待ちますが、asyncio.create_taskではすぐに待たずにprint('ここはすぐに出力される')が実行されます。
タスクは裏で進んでいて、そのあと好きなタイミング後で必要なときにawaitすることができます。