非同期処理の概念は理解できたけれど、「実際にどんな仕組みで動いているの?」と内部の動作原理が気になる方も多いのではないでしょうか。
この記事では、非同期処理の動作原理と処理フローについて、イベントループ・コールバック・タスクキュー・スレッドの仕組みまで詳しく解説しています。
非同期処理をより深く理解して、効率的なプログラム設計に役立てましょう。
非同期処理の動作原理:イベントループの仕組み
それではまず、非同期処理の核心であるイベントループの仕組みについて解説していきます。
JavaScriptの非同期処理の中心にあるのが「イベントループ(Event Loop)」という仕組みです。
JavaScriptはシングルスレッドの言語ですが、イベントループとタスクキューの組み合わせによって、あたかも並行処理しているかのような動作を実現しています。
コールスタック・タスクキュー・イベントループの関係
イベントループの処理サイクル:
1. コールスタックに積まれた同期処理を実行する
2. コールスタックが空になったら、タスクキューの先頭を取り出す
3. 取り出したコールバック(タスク)をコールスタックに積んで実行する
4. コールスタックが空になったら再びタスクキューを確認する
5. 1〜4を繰り返す(これがイベントループ)
タスクキューには、非同期処理(タイマー・HTTP通信・ファイルI/Oなど)が完了したときのコールバック関数が積まれます。
コールスタックが空にならないと(同期処理が終わらないと)タスクキューのコールバックは実行されないため、長い同期処理があるとイベントループがブロックされます。
マクロタスクとマイクロタスクの違い
タスクキューにはマクロタスク(setTimeout・setIntervalなどのコールバック)とマイクロタスク(Promise.then・queueMicrotaskなど)の2種類があります。
実行の優先順位はマイクロタスクが高く、コールスタックが空になるたびにマイクロタスクキューを全て処理してからマクロタスクを取り出す仕組みになっています。
スレッドと非同期処理の関係を解説
続いては、スレッドと非同期処理の関係を確認していきます。
シングルスレッドとイベント駆動モデル
Node.jsやブラウザのJavaScriptはシングルスレッドで動作しますが、I/O処理はOSや組み込みのC++ライブラリ(libuv)が別スレッドで処理し、完了するとイベントキューに通知します。
これが「イベント駆動(Event-Driven)モデル」であり、シングルスレッドでも高い並行性を実現できる理由です。
マルチスレッドとの比較
JavaやC#などのマルチスレッド言語では、時間のかかる処理を別スレッドで実行することで並行処理を実現します。
マルチスレッドはCPUを並列に使える利点がありますが、スレッド間の共有リソースの競合(Race Condition)やデッドロックのリスクがある点がシングルスレッドのイベント駆動モデルとの大きな違いです。
Pythonの非同期処理(asyncio)の仕組み
続いては、Pythonの非同期処理であるasyncioの仕組みを確認していきます。
asyncioのイベントループ
Pythonのasyncioライブラリも独自のイベントループを持ち、コルーチン(async def関数)をスケジュールして実行する仕組みです。
Pythonのasyncioによる非同期処理の例:
import asyncio
async def fetch_data():
print(“データ取得開始”)
await asyncio.sleep(2) # 非同期で2秒待機
print(“データ取得完了”)
return “data”
async def main():
result = await fetch_data()
print(result)
asyncio.run(main())
awaitキーワードで「ここで非同期処理を待つ」と宣言することで、待っている間に他のコルーチンが実行できます。
まとめ
この記事では、非同期処理の動作原理と処理フローについて、イベントループ・コールスタック・タスクキュー・スレッドとの関係まで詳しく解説しました。
非同期処理はイベントループがコールスタックの空き時間にタスクキューのコールバックを実行することで、シングルスレッドでも並行処理的な動作を実現する仕組みです。
今回の内容を参考に、非同期処理の動作原理をしっかり理解して効率的なプログラム設計に活かしていきましょう。