Express(Node.js)で重い処理をマルチスレッド化

# はじめに
Expressでhttpサーバーを作っている中で、重い処理を実行したい場面があります。

例えば、別のサーバーからhttpサーバー内へ何かをダウンロードするとか。

そのリクエストに対して、**即座に「ダウンロードを開始しました」と返し**、裏でダウンロードを走らせる。またダウンロード中にまた同じリクエストが来たとき、**即座に「今ダウンロードしてます」と返し**たい。

ダウンロード処理を非同期化すれば、できるんでしょと思いきや、意外とすんなりできなかったので、その問題点、原因、対策を共有します。

# 問題点：うまくいかなかった方法
重い処理のリクエストがあったら、グローバルの`isLoading`フラグを立て、asyncで定義した関数を呼び出す。終わったら、フラグを倒す。

こんな感じ。
```TypeScript:index.ts
let isProcessing = false;
app.get("/heavyProcess", (req, res) => {
    console.log("/heavyProcessが呼び出されました");
    
    // 既に実行中の場合
    if (isProcessing) {
        res.status(409).json({ 
            message: "重い処理が既に実行中です",
            status: "loading"
        });
        return;
    }
    
    // 重い処理を開始
    isProcessing = true;
    heavyProcess1("hogehoge", "fugafuga")
        .then(() => {
            console.log("重い処理が完了しました");
            isProcessing = false;
        })
        .catch(error => {
            console.error("重い処理でエラーが発生しました:", error);
            isProcessing = false;
        })
    ;
    console.log("重い処理、始めました");
    
    // レスポンスを返す
    res.json({
        message: "重い処理を開始しました",
        status: "started"
    });
});
```
こうすると、サーバーでは"重い処理を始めました"が出て、クライアントにも返ります。

ですがその処理中に、もう一度同じURLを呼ぶと、まったくなにも始まらない。"/heavyProcessが呼び出されました"も出ません。クライアント側は、レスポンス待ちでクルクルします。

これでは、１度目のリクエストは非同期の恩恵を受けますが、２度目のリクエストは同期処理のようなもの。

ラーメン屋の注文でいうと、１度目の人は注文を出して席に座ってスマホをいじれますが、２度目の人は注文すら聞いてもらえず立って待っている感じ。

そういう問題です。

# 原因
Node.jsのシングルスレッド特性により、重い処理がメインスレッドで実行されています。その結果、他のリクエスト処理がブロックされていました。

そのため、`isProcessing`とかのハンドリング以前に、`app.get("/heavyProcess"～`自体を聞いてもらえない状態でした。


# 対策
`worker_threads`を使って、重い処理を別スレッドで実行。メインスレッドでは、Expressサーバーの処理のみを担当。

具体的には、workerの呼び出し部分だけ切り出します。
```TypeScript:index.ts
import path from "path";
import { fileURLToPath } from "url";
import { Worker } from "worker_threads";

// ～～省略～～

app.get("/heavyProcess", (req, res) => {
    // ～省略～
    
    // 重い処理を開始
    isProcessing = true;
    const __dirname = path.dirname(fileURLToPath(import.meta.url));
    const worker = new Worker(path.join(__dirname, "./worker.js"));
    worker.on("message", (message) => {
        if (message.type === "complete") {
            console.log("重い処理が完了しました");
            isLoading = false;
        } else if (message.type === "error") {
            console.error("重い処理でエラーが発生しました:", message.error);
            isLoading = false;
        }
    });
    worker.on("error", (error) => {
        console.error("Workerエラー:", error);
        isLoading = false;
    });
    worker.postMessage({
        type: "heavyProcess1",
        param1: "hogehoge",
        param2: "fugafuga",
    });

    // ～省略～
```

次にworker.tsの記述。
他の重い処理も実行できるように、`data.type`で呼び出す関数をハンドリングできるようにしてあります。今は１つですが。
```TypeScript:worker.ts
import { parentPort } from 'worker_threads';
import { heavyProcess1 } from "./heavyProcess1.js";

parentPort?.on('message', async (data) => {
    try {
        if (data.type === "heavyProcess1") {
            await heavyProcess1(data.param1, data.param2);
            parentPort?.postMessage({ type: 'complete' });
        }
    } catch (error) {
        parentPort?.postMessage({ type: 'error', error });
    }
});
```

この結果、２度目のリクエストがあったとき、"重い処理が既に実行中です"と、サラっとかわされるようになりました。


ラーメン屋の注文でいうと、１度目の人は注文を出して席に座ってスマホをいじれ、２度目の人は注文を出すと「今作ってんだよ、座って待ってな！」と言われる感じ。ラーメン屋と違って、２人とも同じ"重い処理"を待つということが、微妙に違うたとえで恐縮ですが。

# まとめ
"Node.jsのシングルスレッド特性"という言葉は、どこかで聞いたような気がしますが、初めて真に受けました。今後もご挨拶することがあるでしょう。覚えておきたい方法です。

## チャットエージェントの話
この問題は、ChatGPTやPerplexityを使っても、なかなか解決に至りませんでした。

非同期処理を呼び出す方法は正しくて、１つ目のリクエストは正しくハンドリングされており問題はない。２つ目が待ってしまう状況の把握と対応、というためでしょう。

AIに尋ねると、
- １つ目のリクエストの問題解決（うまくいっているのに改悪）
- １つ目を速く返す方法（heavyProcessのチューニング）
- ２つ目を速く返す方法（timeoutを短くする）

といった回答に振り回されました。都度「そうじゃなくて」って軌道修正しないといけない。私のプロンプトの出し方にも問題があるだろうし、**聞く側が問題を正しく理解しないと正しい解決策が出ない**ということを思い知りました。

今の段階では人間は伝え方を練習しないといけないし、そのうちAIが賢くなることで解決できるようになるかもしれません。


では、ゆっくり充実した生活を！