ハンズオンNode.js
https://scrapbox.io/files/615f3ff083be8e001fffb59b.png
エントリポイントがフロントエンドツール動かすためのJS実行環境だったのでサーバーとしての用途がわからない
そもそもWebアプリケーションサーバーについて学んだことがない
Node.jsはイベントループで並行処理する
スレッドが増えると性能が極端に劣化して困る
スレッド生成や切り替えにコストがかかる
イベントループはシングルスレッド
タスクがキューに並んでいる
タスクを1つずつ実行する
さらにI/Oを実行する
I/Oの完了後にやるタスクが指定されている
I/Oが終わると指定されていたタスクがキューに突っ込まれる
スモールコア
battery includedの対義語っぽい
標準では低レベルな機能だけを提供する
これが必ずしもいいものなのかはわからないが…
ひとつのことをうまくやる
協調して動く
テキストストリームを使う
Node.jsモジュールもストリームをインターフェースとして使うことが多い
ユニバーサルJavaScript
いろんな環境で動くJavaScriptコード
Console API (console)
Timer API (setTimeout() など)
REPL で .editor と入力するとエディタモードに入る!?!?知らんわ!! _ が直前の式の評価結果を _error が直近のエラーを参照できる!?!?
2. 非同期プログラミング
マルチスレッドでは実行環境が勝手に並行処理してくれる
ブロッキング
時間がかかってプログラムを止める処理
code:blocking.js
// ブロッキングI/Oの例
// 普通に関数呼び出しをしている
const f = fs.readFileSync("./README.md", "utf-8"); // ここでブロッキングI/O
console.log(f);
普通の処理とブロッキング処理の間に構文上の違いがない
マルチスレッドではコードが単純になる
そもそも普通の処理というのもごくごく短時間ながら実行がそこで止まっている
ブロッキングしてるなーと思ったら別スレッドに切り替えて別の処理をやってくれる
スレッド切り替え時に状態を保存したり復元したりする
たしかに
イベントループはシングルスレッドだからマルチスレッドの問題がないぜ!
並行処理は本質的に難しいんだがな!
code:nonblocking.js
fs.readFile("./README.md", "utf-8", (err, f) => {
// I/O完了後に実行されるタスクがここ
console.log(f);
});
// I/Oを待たずにここから先が即座に実行される
// 地味ながら、I/O待ちの間にここを処理するのはブロッキングI/Oではできない
その完了を待たずに次に進んでしまう処理
同期処理
その完了を待ってプログラムが停止する処理
イベントループで並行処理するためにはI/Oはノンブロッキングでなければならないらしい
このように同期処理と非同期処理で構文が変わる
イベントループではコードが複雑化する
タスクは順番に処理されるので、タスクがデカい (CPU負荷が高い) と止まる
このようなときはマルチスレッドが向く
なのでNode.jsでは必要に応じてマルチスレッドもできる
非同期処理の後に実行するタスクを関数 (コールバック関数) として指定する
code:cb.js
setTimeout(() => {
console.log("1秒経過");
}, 1000); // 1000ms
console.log("setTimeoutを実行"); // こっちが先に実行される
0ミリ秒待つようにしても後続の処理が先に実行される
code:c.js
setTimeout(() => console.log("setTimeoutのコールバック"), 0);
console.log("setTimeout実行");
なんでじゃ
コールバック=>非同期 ではない
code:fe.js
1, 2, 3.forEach(v => console.log(v)); console.log("forEach実行"); // これは後から実行される
Node.jsの非同期コールバックには規約がある
コールバックは最後の引数
コールバックの引数は最初がエラーでそれ以降が処理の結果
util.promisify などがこの規約
コールバックの中で発生したエラーは外からtry-catchできない
code:throw.js
const divAfter = (ms, x, y, callback) => {
try {
setTimeout(() => callback(x / y), ms);
} catch (e) {
callback(null);
}
// ここにconsole.log入れたらsetTimeoutのコールバックより先に実行される
};
// ゼロ除算で死ぬ (numberではInfinityになるのでbigintでやる)
divAfter(1000, 10n, 0n, value => console.log(value));
コールバックの呼び出し元はイベントループ
code:コールスタック
RangeError: Division by zero
at Timeout._onTimeout (/tmp/node/code.js:3:33)
at listOnTimeout (node:internal/timers:557:17)
at processTimers (node:internal/timers:500:7)
そもそも非同期処理に合わせてcatch節のコードが走ったらちょっと気持ち悪い
try-catchする場所を変えよう
コールバック内、イベントループに到達するより前でcatchしてしまう
現在のコードが非同期コールバック関数定義だったらcatchしたエラーをcallbackに渡す
code:throw.js
const divAfter = (ms, x, y, callback) => {
setTimeout(() => {
try {
callback(null, x / y);
} catch (e) {
callback(e, null)
}
}, ms);
};
divAfter(1000, 10n, 0n, (err, value) => {
if (err != null) console.error(err: ${err}), process.exit(1);
console.log(value)
});
err,value を受け取って分岐するのがGoのエラー処理に似ているという意見を見た 状況によって同期的だったり非同期的だったりする関数を書くな
code:bad.js
// 前のdivAfterの実装を使う
const divAfterSafe = (ms, x, y, callback) => {
// 0nの場合同期的にコールバックを呼んでしまっている
if (y === 0n) callback(null, 0n);
else divAfter(ms, x, y, callback);
};
divAfterSafe(1000, 10n, 0n, (err, value) => {
if (err != null) console.error(err: ${err}), process.exit(1);
console.log(value);
});
console.log("10n / 0nを実行"); // ここは上のコールバックの後に実行される
divAfterSafe(1000, 10n, 10n, (err, value) => {
if (err != null) console.error(err: ${err}), process.exit(1);
console.log(value);
});
console.log("10n / 10nを実行"); // ここは上のコールバックの前に実行される
同期/非同期によって実行順が前後して最悪になる
かならず非同期にしよう
Node.jsのイベントループには6つフェーズがあり、即座に実行される非同期処理でもそれらの間にタイミングの違いがある
setTimeout(cb, 0)
timersフェーズで実行される
Web標準に類似したAPIがあるのでブラウザでもたぶん動く
queueMicrotask(cb)
microTaskQueueに積まれフェーズとフェーズの間で実行される
Web標準なのでブラウザでも動く
Promise.resolve().then(cb)
microTaskQueue
ECMAScript標準
microTaskQueueという存在はECMAScriptなのだろうか?
process.nextTick(cb)
nextTickQueueに積まれフェーズとフェーズの間でmicroTaskQueueより先に実行される
コールバックの結果をさらにコールバックで使うと地獄が発生する
code:cbhell.js
f(arg, (err, result) => {
if (err != null) /* エラーハンドリング */;
g(result, (err, result) => {
if (err != null) /* エラーハンドリング */;
h(result, (err, result) => {
if (err != null) /* エラーハンドリング */;
console.log(result);
});
});
});
さっきのhandleが言語組み込みになったやつ
code:await.js
// async関数
async function main() {
try {
// Promiseをawaitすると解決するまで関数が止まるのでfileはPromiseの結果を受け取れる
const file = await fs.readFile("./.prettierrc", "utf-8");
const parsed = await parseJSONAsync(file);
return parsed;
}
catch(e) {
// エラーハンドリング
}
}
async関数の返り値はかならずPromise
for await of
非同期反復処理
Asynciterable ([Symbol.asyncIterator] メソッドを実装したオブジェクト)
code:asyncgen.mjs
import { setTimeout } from "timers/promises";
async function* countup(start, end) {
for (let count = start; count < end; count++) {
await setTimeout(Math.random() * 1000);
yield count;
}
}
for await (const i of countup(0, 16)) {
console.log(i);
}
3. EventEmitterとストリーム
4. マルチプロセス、マルチスレッド
マルチコアシステムでは複数のプロセスやスレッドを並列に実行できる 逆に、イベントループでの並行処理はシングルスレッドなので、マルチコアではそのまま動かしてもリソースを有効活用できない
cluster モジュールでマルチプロセス
worker_threads モジュールでマルチスレッド
並行処理…マルチスレッド、イベントループ
並列処理…マルチコアでのマルチスレッド、マルチプロセス
Webアプリケーションなどはマルチプロセスで動かす
code:fibonacci.mjs
export function fibonacci(n) {
return n <= 1 ? n : fibonacci(n - 1) + fibonacci(n - 2);
}
code:web-app.mjs
import http from "node:http";
import { fibonacci } from "./fibonacci.mjs";
http
.createServer((req, res) => {
const n = Number(req.url.substr(1));
if (Number.isNaN(n)) return res.end();
const result = fibonacci(n);
res.end(result.toString());
})
.listen(3000);
code:multi-process.mjs
import cluster from "node:cluster";
import url from "node:url";
import path from "node:path";
import os from "node:os";
console.log("main process:", process.pid);
const __dirname = path.dirname(url.fileURLToPath(import.meta.url));
cluster.setupPrimary({
exec: ${__dirname}/web-app.mjs,
});
const cpuList = os.cpus();
for (const _ of cpuList) {
const sub = cluster.fork();
console.log( sub process:, sub.process.pid);
}
npx loadtest -c 100 -t 10 http://localhost:3000/30 したところ、マルチプロセスだと6倍弱多くのリクエストを処理した