シングルスレッドとは?

JavaScriptは、基本的にシングルスレッドという仕組みで動作します。これは、一度に1つのJavaScript処理だけを実行するという意味です。
通常のコードは上から順番に実行され、1つの処理が終わるまで次の処理は実行されません。この仕組みはシンプルで分かりやすい一方、時間のかかる処理があると、その間は他の処理が待たされてしまうという問題があります。
特にブラウザ上では、重いJavaScript処理が続くと、画面の更新やクリック操作が止まったように見えることがあります。
シングルスレッドと非同期処理
JavaScriptはシングルスレッドで動作しますが、非同期処理を使うことで、時間のかかる処理を待っている間にも他の処理を進められます。
非同期処理は、JavaScriptが同時に複数の処理を実行しているという意味ではありません。実際には、ブラウザやNode.jsなどの実行環境がタイマー処理や通信処理を管理し、完了したタイミングでJavaScriptに結果を戻しています。
この仕組みによって、JavaScriptはシングルスレッドでありながら、ユーザー操作を妨げにくい処理を実現できます。
同期処理と非同期処理の違い
以下の表は、同期処理と非同期処理の違いを示しています。
| 処理の種類 | 特徴 |
|---|---|
| 同期処理 | 一度に1つの処理を順番に実行し、前の処理が完了するまで次の処理が開始されない。 |
| 非同期処理 | 時間のかかる処理を待たずに次の処理へ進み、処理が完了したときに結果を受け取る。 |
非同期処理の簡単な例
console.log("処理1");
setTimeout(() => {
console.log("処理2");
}, 2000);
console.log("処理3");
このコードの実行結果は、次のようになります。
処理1
処理3
処理2
setTimeout()の中の処理は2秒後に実行されるため、その間に「処理3」が先に実行されます。
コールバック関数とは?
コールバック関数とは、ある処理が完了したあとに呼び出される関数のことです。
非同期処理では、処理の完了タイミングがすぐには分からないことがあります。そのため、「処理が終わったらこの関数を実行する」という形で、コールバック関数を渡すことがあります。
コールバック関数の基本的な使い方
function fetchData(callback) {
setTimeout(() => {
console.log("データを取得しました");
callback();
}, 2000);
}
function processData() {
console.log("データを処理します");
}
fetchData(processData);
この例では、fetchData()の処理が終わったあとに、processData()が実行されます。
プログラムの解説
fetchData(processData)を実行する。setTimeout()によって、2秒後に処理が実行される。- 「データを取得しました」と表示される。
- コールバック関数として渡された
processData()が実行される。 - 「データを処理します」と表示される。
シングルスレッド環境でのコールバックのメリット
シングルスレッド環境において、コールバック関数を使うメリットには以下のようなものがあります。
| メリット | 説明 |
|---|---|
| 長時間の処理を待たずに済む | 通信やタイマー処理などを待っている間にも、他の処理を進められる。 |
| UIが止まりにくい | 非同期処理を使うことで、ユーザー操作への反応を維持しやすくなる。 |
| 処理完了後の動作を指定できる | 処理が終わったタイミングで、必要な関数を実行できる。 |
コールバック地獄に注意
コールバック関数は便利ですが、処理が増えるとコードが深くネストし、読みにくくなることがあります。
このような状態は、コールバック地獄(Callback Hell)と呼ばれます。
fetchData(() => {
processData(() => {
renderData(() => {
saveData(() => {
console.log("データを保存しました");
});
});
});
});
このようにコールバック関数が何重にも入れ子になると、処理の流れを追いにくくなります。また、エラー処理も複雑になりやすくなります。
コールバックからPromise、async/awaitへの進化
現在のJavaScriptでは、コールバックだけでなく、Promiseやasync/awaitを使って非同期処理を書くことが一般的です。
Promiseやasync/awaitを使うと、非同期処理の流れをより読みやすく整理できます。
Promiseの例
function fetchData() {
return new Promise((resolve) => {
setTimeout(() => {
console.log("データを取得しました");
resolve();
}, 2000);
});
}
fetchData().then(() => {
console.log("データを処理します");
});
Promiseを使うと、処理が成功した後の流れを.then()で記述できます。
async/awaitの例
function fetchData() {
return new Promise((resolve) => {
setTimeout(() => {
console.log("データを取得しました");
resolve();
}, 2000);
});
}
async function processData() {
await fetchData();
console.log("データを処理します");
}
processData();
async/awaitを使うと、非同期処理であっても、上から順番に処理が進むような形で書けます。そのため、Promiseチェーンよりも直感的に理解しやすくなります。
イベントループとの関係
JavaScriptがシングルスレッドでありながら非同期処理を扱えるのは、イベントループという仕組みがあるためです。
イベントループは、現在実行中の処理が終わったあと、待機している非同期処理の続きを実行する仕組みです。
console.log("開始");
setTimeout(() => {
console.log("非同期処理");
}, 0);
console.log("終了");
実行結果は、次のようになります。
開始
終了
非同期処理
setTimeout()に0を指定しても、すぐに実行されるわけではありません。まず現在の同期処理が終わり、その後で非同期処理が実行されます。
重い処理には注意
非同期処理を使っても、すべての処理が自動的に軽くなるわけではありません。
たとえば、非常に重い計算処理をメインスレッドで実行すると、その間は画面の更新やクリック操作が止まることがあります。
for (let i = 0; i < 1000000000; i++) {
// 重い処理
}
console.log("処理完了");
このような重い処理が必要な場合は、処理を分割したり、ブラウザではWeb Workerを使って別スレッドで処理することも検討します。
まとめ
- JavaScriptは基本的にシングルスレッドで動作します。
- シングルスレッドでは、一度に1つのJavaScript処理だけが実行されます。
- 非同期処理を使うと、時間のかかる処理を待っている間にも他の処理を進められます。
- コールバック関数は、処理が完了した後に実行される関数です。
- コールバックが深くネストすると、コードが読みにくくなることがあります。
- 現在のJavaScriptでは、Promiseやasync/awaitを使って非同期処理を書くことが一般的です。
- イベントループによって、シングルスレッドでも非同期処理を効率よく扱えます。
- 重い計算処理は画面を止める原因になるため、処理の分割やWeb Workerの利用も検討しましょう。