非同期処理の基本 (非同期処理 + Callback 関数) ~ JavaScript の非同期を理解する 第 5 弾

## 非同期処理の基本 (非同期処理 + Callback 関数)

JavaScript の非同期を理解する記事の第 5 弾です。
ここまで読んでいただいた皆様、ありがとうございます。すでにシリーズの半分が過ぎましたが、ここでようやく 非同期処理 + Callback 関数について解説します。
まずは 非同期処理の基本を学んでいきたいと思います。

### そもそも非同期処理とは何か

まず初めに、非同期処理とは何かを理解していきます。
普段、エンジニアをしていると似たような言葉を聞くことが多いです。

- 並列処理
- 並行処理
- 非同期処理

そもそもこれらは何が違うのでしょうか？まずは用語の定義を Wiki などで見てみましょう。

> The concept of concurrent computing is frequently confused with the related but distinct concept of parallel computing, although both can be described as "multiple processes executing during the same period of time". In parallel computing, execution occurs at the same physical instant: for example, on separate processors of a multi-processor machine, with the goal of speeding up computations—parallel computing is impossible on a (one-core) single processor, as only one computation can occur at any instant (during any single clock cycle).[a] By contrast, concurrent computing consists of process lifetimes overlapping, but execution does not happen at the same instant. The goal here is to model processes that happen concurrently, like multiple clients accessing a server at the same time. Structuring software systems as composed of multiple concurrent, communicating parts can be useful for tackling complexity, regardless of whether the parts can be executed in parallel.[5]: 1 
> [Wikipedia - Concurrent computing](https://en.wikipedia.org/wiki/Concurrent_computing) 2025/04/23

ここではまさに、並列処理と並行処理の違いについて記載されています。要約すると、並列処理というのは、物理的な同じ時間に、複数の処理を実行することです。一方で、並行処理というのは、時分割で論理的に同時に実行することです。そのため、並行処理というのは、処理系が 1 つの CPU である場合でも、複数の処理を同時並行に実行することができます。これは、例えば、複数人が 1 つのサービスにアクセスしている場合、それぞれの人が並行してサービスを利用しているようなイメージです。CPU が 1 つでも、処理を細かく切り替えて実行することで、複数のアクセスを同時に処理することができます。

一方で非同期については以下のように記述されています。

> Asynchrony, in computer programming, refers to the occurrence of events independent of the main program flow and ways to deal with such events. These may be "outside" events such as the arrival of signals, or actions instigated by a program that take place concurrently with program execution, without the program hanging to wait for results.[1] Asynchronous input/output is an example of the latter case of asynchrony, and lets programs issue commands to storage or network devices that service these requests while the processor continues executing the program. Doing so provides a degree of concurrency.[1] > [Wikipedia - Asynchrony](https://en.wikipedia.org/wiki/Asynchrony) 2025/04/23

こちらは、非同期について書かれていますが、要約すると、非同期というのは、プログラムのメイン・フローと依存しないイベントの処理方法のこと、とのことです。例えば、非同期の I/O 処理を行い、その処理を、メイン処理を妨げることなく実行すること。と記載されています。非同期的に処理を行うことで、ある程度の並行性が得られるということです。

まとめると以下になります。

**並列処理 (concurrent programming)**

- 物理的な同じ時間に、複数の処理を実行すること
- CPU の数が可能な並列処理数の上限となる

**並行処理 (parallel programming)**

- 時分割で論理的に同時に実行すること
- 処理系が 1 つの CPU である場合でも、複数の処理を同時並行に実行することができる

**非同期 (asynchronous)**

- プログラムのメイン・フローと依存しないイベントの処理方法のこと
- 非同期に処理を行うことで、ある程度の並行性が得られる

ここまでみていくと、並列、並行処理は、「どのように処理をするか」という処理系の話であるのに対し、非同期処理とは、「イベントの処理方法」という処理制御について指しているということがわかります。

並列、並行処理についてはこれ以上詳しく解説しませんが私が以前に別記事で少し解説したので、よろしければそちらをご覧ください。

[Python での並列、並行処理について勉強、実装してみた](https://qiita.com/ZawaP/items/666a4a0674cdb5858fdb)

改めてこれらの違いを以下の観点から比較、まとめると以下となります。

- 実行タイミング
- 実行順序
- データ共有

| 観点           | 並列処理（Parallel）    | 並行処理（Concurrent） | 非同期処理（Asynchronous）     |
| -------------- | ----------------------- | ---------------------- | ------------------------------ |
| 実行タイミング | 複数 CPU で物理的に同時 | 時分割で論理的に同時   | 処理完了を待たずに次へ進む     |
| 実行順序       | 非決定的（完全に並列）  | スケジューラによる     | 明示的制御（callback など）    |
| データ共有     | 必要（競合あり）        | 必要（排他制御が必要） | 最小限（シングルスレッド前提） |

### Callback 関数

#### Web APIs について

非同期処理が何たるかを理解したところで、実際に Web ブラウザ上で JavaScript を実行する場合について考えてみます。
JavaScript の実行制御は、シングルスレッドの実行モデルに基づき、**イベントループ（Event Loop）**によって管理されています。ここで重要なのは、JavaScript エンジン（たとえば V8）は 純粋な計算エンジンであり、時間待機・I/O 処理・ネットワーク通信などの非同期機能は自前で持っていないという点です。
これらの機能は、Web ブラウザが提供する Web APIs を利用することで実現しています。(例: `setTimeout`, `fetch`, `addEventListener`, `console.log` など)

#### 同期処理 vs 非同期処理

Web APIs にも同期処理と非同期処理が存在します。例えば、`setTimeout`は非同期処理ですが、`console.log`は同期処理です。
それぞれの場合にどのように処理されるかを見ていきます。

**同期処理 (例: console.log) の場合**

1. Call Stack に処理が積まれる
2. Stack のトップにある Task として処理が開始される
3. Web APIs に処理が移譲される。
4. Web APIs の処理が完了し、結果が返却される
5. 処理終了後、Call Stack から pop される

**非同期処理（例: setTimeout）の場合**

1. Call Stack に処理が積まれる。
2. Stack のトップにある Task として処理が開始される
3. 内部的に Web API（Timer）へ処理が登録される
4. setTimeout 自体の関数呼び出しはすぐに完了し、Call Stack から pop される
5. Web APIs の処理が完了すると、指定されたコールバック関数が Task Queue に登録される
6. Event loop が Call Stack を空と認識したタイミングで、Task Queue から次のタスクを取り出し実行する

このように、非同期処理では「処理の発火」と「処理の完了」は時間的に切り離されており、Callback 関数は、非同期処理を行う関数とは別の Task として管理されます。非同期に処理を行う Web APIs の実際の処理はブラウザ側に移譲され、完了するまで `Event loop`からは切り離されています。これはまさに、非同期の定義に沿った内容であり、非同期処理と呼ばれている理由です。

#### Callback 関数について

さて、上記の処理で新しい単語 Callback 関数が出てきました。
まず一言で言ってしまうと、Callback 関数とは、他の関数に引数として渡される関数のことです。非同期処理と組み合わせることで、非同期処理が完了した後に、実行される関数を指定することができます。

上記のように非同期処理が完了すると、指定された Callback 関数が `Task Queue`に追加されます。ここからさらに非同期関数を実行することもできますし、非同期処理をネストすることもできます。また、非同期処理を複数呼ぶと実行順序が保証されないのは、Web APIs の処理が完了するまで `Event loop` & `Task Queue`から切り離されているためです。

### コードを実行してみよう

**単純な Callback 関数の例**

```javascript
// これは Callback 関数の例です。
console.log("start");
setTimeout(() => {
  console.log("Callback Task 1");
}, 1000);
console.log("end");
```

上記では、`setTimeout` の結果として、1000ms 後に `Callback Task 1` が実行されます。
実際には、`setTimeout` の処理は Web APIs に移譲され、1000ms 後に `Callback Task 1` が `Task Queue` に追加されています。

**setTimeout 0ms でも非同期で実行される例**

```javascript
// setTimeout 0ms でも非同期処理が行われる
console.log("start");
setTimeout(() => {
  console.log("Callback Task 2");
}, 0);
console.log("end");
```

上記では、`setTimeout` の結果として、0ms 後に `Callback Task 2` が実行されます。
`setTimeout` の結果として、0ms を指定していますが、Callback 関数は `Task Queue` に一度追加されるため、実行は非同期となります。

**複数の Callback 関数と実行順序の例**

```javascript
// 実行順序が時間によって決まる例
console.log("開始");

setTimeout(() => {
  console.log("3秒後のコールバック");
}, 3000);

setTimeout(() => {
  console.log("1秒後のコールバック");
}, 1000);

console.log("終了");

// 出力:
// 開始
// 終了
// 1秒後のコールバック
// 3秒後のコールバック
```

** Callback 関数を渡す & 受け取る例**

```javascript
// コールバック関数を受け取る関数を定義
function Hello(name, callback) {
  console.log(`Hello, ${name}!`);
  callback();
}

// 関数を呼び出し、コールバックを渡す
Hello("John", () => {
  console.log("コールバックが実行されました");
});
```

## まとめ

この記事では、JavaScript の Callback 関数について理解しました。

- 並列、並行、非同期の違い
- 非同期処理 + Callback 関数について

普段の仕事で 非同期処理 + Callback 関数を直接書くことはあまりないと思いますが、これは決して重要性が低下したわけではありません。むしろ、非同期処理 + Callback 関数とその背景の内部処理を理解することは非同期処理を理解する上で非常に重要だと個人的には思っています。非同期処理はここから始まり、jQuery による実装、そして Promise の実装とより便利な形に進化していきました。

### 次回

この記事では JavaScript の 非同期処理 + Callback 関数について解説しました。
次回は、jQuery による非同期処理 についてまとめていきます。

- Callback 関数に欠点はないの？
- jQuery Deferred がなぜうまれたのか

そんな疑問に答える第 6 章は [**「jQuery による非同期処理」**](https://qiita.com/ZawaP/private/ffdde0a83f322002642c)です。

(\*この記事は、JavaScript について勉強した内容をまとめたものであり、内容が不正確な可能性があります。もし指摘などあれば、コメントいただけるととても嬉しいです。)

### 参考資料

[Callback function](hhttps://developer.mozilla.org/en-US/docs/Glossary/Callback_function)