その他の非同期処理テクニック:Generator、Observable、Web Worker
JavaScriptはシングルスレッドなの? 非同期処理とイベントループで探る真相
この仕組みを理解することで、JavaScriptにおける「シングルスレッド」の本当の意味と、非同期処理を駆使した高速で滑らかなWebアプリケーション開発が可能になります。
シングルスレッドの制約:1つのタスクずつ順番に処理
従来のプログラミング言語では、マルチスレッドと呼ばれる仕組みで複数のタスクを並行して処理することが一般的でした。一方、JavaScriptはシングルスレッドであり、一度に処理できるタスクは1つだけという制約があります。
例えば、以下のようなコードを実行した場合を考えてみましょう。
function task1() {
for (let i = 0; i < 1000000; i++) {
console.log("task1");
}
}
function task2() {
for (let i = 0; i < 1000000; i++) {
console.log("task2");
}
}
task1();
task2();
このコードでは、task1
とtask2
という2つの関数が呼び出されています。しかし、シングルスレッドであるため、実際には順番に処理されます。つまり、
task1
が100万回ループ処理を実行
という順番になります。
この制約により、CPU密集型の処理を実行すると、他のタスクがブロックされてしまうという問題が発生します。例えば、画像処理や計算処理など、時間がかかる処理を実行すると、ユーザーインターフェースが反応しなくなる可能性があります。
非同期処理とイベントループで疑似的な並行処理を実現
しかし、JavaScriptには非同期処理と呼ばれる仕組みがあります。非同期処理とは、タスクを完了してから結果を通知するという方法です。具体的には、以下のような方法があります。
- コールバック関数: 関数を実行し、その関数が完了した後に別の処理を実行する
- Promise: 非同期処理の結果を非同期的に取得する
- async/await: 非同期処理を同期的に記述する
これらの仕組みを活用することで、あたかも並行処理しているかのように動作させることができます。
さらに、イベントループと呼ばれる仕組みも重要です。イベントループは、ブラウザが処理すべきイベントを管理する仕組みです。例えば、ユーザーがクリックしたとき、タイマーが終了したときなど、様々なイベントが発生します。イベントループは、これらのイベントをキューに貯め、順番に処理していきます。
JavaScriptはシングルスレッドですが、非同期処理とイベントループを組み合わせることで、ユーザーインターフェースを滑らかに動作させながら、CPU密集型の処理を実行することができます。
シングルスレッドのメリットとデメリット
メリット
- シンプルで理解しやすい: マルチスレッドに比べてコードがシンプルで理解しやすい
- デバッグしやすい: スレッド間の競合などの問題が発生しにくい
- 軽量: マルチスレッドに比べてメモリ使用量が少ない
デメリット
- CPU密集型の処理が遅くなる: CPU密集型の処理を実行すると、他のタスクがブロックされてしまう
- 複雑な処理には向かない: 複雑な並行処理を記述するには、非同期処理とイベントループの仕組みを深く理解する必要がある
まとめ
JavaScriptはシングルスレッドですが、非同期処理とイベントループを活用することで、あたかも並行処理しているかのように動作させることができます。この仕組みを理解することで、高速で滑らかなWebアプリケーション開発が可能になります。
ただし、シングルスレッドにはCPU密集型の処理が遅くなるというデメリットもあります。複雑なアプリケーション開発を行う場合は、この点を考慮する必要があります。
参考情報
- [JavaScriptにおける非同期処理
JavaScriptにおける非同期処理サンプルコード
コールバック関数を使用した非同期処理
function task1(callback) {
setTimeout(() => {
console.log("task1");
callback();
}, 1000);
}
function task2() {
console.log("task2");
}
task1(task2); // task1が完了したらtask2を実行
Promiseを使用した非同期処理
function task1() {
return new Promise((resolve) => {
setTimeout(() => {
console.log("task1");
resolve();
}, 1000);
});
}
function task2() {
console.log("task2");
}
task1().then(task2); // task1が完了したらtask2を実行
このコードでは、task1
関数の中で非同期処理を行い、完了したらPromiseオブジェクトを返しています。then
メソッドを使用して、Promiseオブジェクトが完了したときに実行する関数を指定することができます。task2
関数をthen
メソッドの引数として渡すことで、task1
が完了した後にtask2
が実行されます。
async/awaitを使用した非同期処理
async function main() {
await task1();
task2();
}
async function task1() {
console.log("task1");
await new Promise((resolve) => {
setTimeout(resolve, 1000);
});
}
function task2() {
console.log("task2");
}
main();
このコードでは、async/await
構文を使用して非同期処理を記述しています。async
キーワードを関数名の前に記述することで、その関数が非同期処理を実行できることを示します。await
キーワードを非同期処理の前に記述することで、その処理が完了するまで待機することができます。
これらのサンプルコードは、JavaScriptにおける非同期処理の基本的な仕組みを理解するのに役立ちます。
- 複数の非同期処理を順番に実行する
- エラー処理を行う
これらのサンプルコードは、以下のURLで確認することができます。
これらのサンプルコードを参考に、さまざまな非同期処理を駆使したWebアプリケーション開発に挑戦してみてください。
JavaScriptで非同期処理を行うその他の方法
Generator
- ジェネレータ関数は、イテレータオブジェクトを生成する関数です。
- イテレータオブジェクトは、
next()
メソッドを呼び出すことで、順番に値を返すことができます。 - 非同期処理をイテレータとして表現することで、コードをより分かりやすく記述することができます。
function* main() {
const result1 = yield task1();
console.log(result1);
const result2 = yield task2();
console.log(result2);
}
async function task1() {
const data = await fetch('https://api.example.com/data1');
return data.json();
}
async function task2() {
const data = await fetch('https://api.example.com/data2');
return data.json();
}
const generator = main();
let result = generator.next();
while (!result.done) {
result = generator.next(result.value);
}
Observable
- Observableは、時間経過とともに値を発行するストリームを表すオブジェクトです。
- 購読者は、
subscribe()
メソッドを使用してObservableに購読し、発行される値を受け取ることができます。
const observable = Rx.Observable.create(observer => {
task1().then(data => observer.onNext(data));
task2().then(data => observer.onNext(data));
observer.onCompleted();
});
observable.subscribe(
data => console.log(data),
error => console.error(error),
() => console.log('完了しました')
);
Web Worker
- Web Workerは、メインスレッドとは別のスレッドで実行されるスクリプトです。
- CPU密集型の処理をWeb Workerで実行することで、メインスレッドをブロックせずに処理することができます。
- ブラウザの描画やユーザーインターフェース操作などのメインスレッドの処理を妨げずに、重い処理を実行したい場合に有効です。
const worker = new Worker('worker.js');
worker.postMessage({ task: 'task1' });
worker.onmessage = (event) => {
console.log(event.data);
worker.postMessage({ task: 'task2' });
};
EventTarget
- EventTargetは、イベントを発行および処理するためのオブジェクトです。
const target = new EventTarget();
target.addEventListener('task1', (event) => {
console.log(event.data);
});
target.addEventListener('task2', (event) => {
console.log(event.data);
});
setTimeout(() => {
target.dispatchEvent(new Event('task1', { data: 'task1完了' }));
}, 1000);
setTimeout(() => {
target.dispatchEvent(new Event('task2', { data: 'task2完了' }));
}, 2000);
これらの方法は、それぞれ異なる特徴 and 利点があります。状況に合わせて適切な方法を選択することで、より効率的で分かりやすいコードを書くことができます。
javascript concurrency