14
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

[小ネタ] Webサーバ、Node.js は無限ループで停止するが Go は停止しない

14
Last updated at Posted at 2026-03-24

はじめに

それぞれNode.jsとGoでは以下のように並行処理方式が異なるので、無限ループに遭遇すると、理屈で考えると以下のようになるはずですが……

  • Node.js: mainスレッドが一つのみの実質的なシングルスレッドなので、サーバ全体が停止します
    • 回避するためには cluster などで複数プロセスで処理します
  • Go: goroutineで継続を複数スレッドに柔軟に当てられるので、サーバが簡単には停止しません
    • 更にプリエンプティブ1なので、処理の最中だろうが処理を中断することができます

本当にそうなのか確かめたことがないので、他ランタイムも含めて実験してみました。

検証方法

各言語で以下の仕様の極力簡素なHTTPサーバを用意します。

  • GET /: OK を返す(正常エンドポイント)
  • GET /loop: 無限ループに入る(問題エンドポイント)

手順は単純で、/loop を叩いた後に / が応答を返すかどうかで判定します。

$ curl http://localhost:PORT/       # まず正常に動くことを確認
OK
$ curl http://localhost:PORT/loop & # 無限ループを発火
$ curl -m 3 http://localhost:PORT/  # 再度リクエスト(3秒でタイムアウト)

検証環境: macOS 15.7.4 / Apple M2 Max (12コア) / arm64

各言語ごとの無限ループ時の反応

Node.js

node/server.mjs
// Node.js v24.13.1
import { createServer } from "node:http";

const server = createServer((req, res) => {
  if (req.url === "/loop") {
    while (true) {}
  }
  res.writeHead(200);
  res.end("OK");
});

server.listen(3001, () => console.log("Node.js listening on :3001"));
実行結果
$ curl http://localhost:3001/
OK

$ curl http://localhost:3001/loop &

$ curl -m 3 http://localhost:3001/
curl: (28) Operation timed out after 3001 milliseconds with 0 bytes received

無限ループでサーバ全体が停止しました。

/loop にアクセスした瞬間にイベントループがブロックされ、他のリクエストも一切処理できなくなりました。

worker_threadsclusterで回避方法はあります)

Go

go/main.go
// Go 1.25.7
package main

import (
	"fmt"
	"net/http"
)

func main() {
	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprint(w, "OK")
	})
	http.HandleFunc("/loop", func(w http.ResponseWriter, r *http.Request) {
		for {
		}
	})
	fmt.Println("Go listening on :3002")
	http.ListenAndServe(":3002", nil)
}
実行結果
$ curl http://localhost:3002/
OK

$ curl http://localhost:3002/loop &

$ curl -m 3 http://localhost:3002/
OK

無限ループでサーバが停止しませんでした。

無限ループは一つの goroutine 内で起きているだけで、他のリクエストは別の OS スレッド上の goroutine で処理されます。

では、これを12本の無限ループに増やすと……

12本同時に /loop を叩いた場合
$ for i in $(seq 1 12); do curl http://localhost:3002/loop & done

$ curl -m 5 http://localhost:3002/
OK

一定量の無限ループ下でも問題なく応答しました。

Go 1.14以降はgoroutineが非協調的にプリエンプションされるため、無限ループ中でもスケジューラがシグナルを送ってスレッドを奪い返せます。

Python (asyncio)

python/server.py
# /// script
# requires-python = ">=3.12"
# dependencies = [
#     "aiohttp",
# ]
# ///
from aiohttp import web

async def handle_ok(request):
    return web.Response(text="OK")

async def handle_loop(request):
    while True:
        pass

app = web.Application()
app.router.add_get("/", handle_ok)
app.router.add_get("/loop", handle_loop)

if __name__ == "__main__":
    web.run_app(app, port=3003)
実行結果
$ uv run python/server.py &

$ curl http://localhost:3003/
OK

$ curl http://localhost:3003/loop &

$ curl -m 3 http://localhost:3003/
curl: (28) Operation timed out after 3001 milliseconds with 0 bytes received

Node.jsと同様に無限ループでサーバ全体が停止しました。

Node.js同様にシングルスレッドのイベントループという並行処理モデル自体の制約です。

run_in_executor()等で回避方法はいくつかあります)

最近のJava (Virtual Thread)

java/Server.java
// openjdk version "26" 2026-03-17
import com.sun.net.httpserver.HttpServer;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.util.concurrent.Executors;

void main() throws IOException {
    var server = HttpServer.create(new InetSocketAddress(3004), 0);
    server.setExecutor(Executors.newVirtualThreadPerTaskExecutor());

    server.createContext("/", exchange -> {
        var response = "OK".getBytes();
        exchange.sendResponseHeaders(200, response.length);
        try (var os = exchange.getResponseBody()) {
            os.write(response);
        }
    });

    server.createContext("/loop", exchange -> {
        while (true) {}
    });

    System.out.println("Java (Virtual Thread) listening on :3004");
    server.start();
}
実行結果
$ java java/Server.java &

$ curl http://localhost:3004/
OK

$ curl http://localhost:3004/loop &

$ curl -m 3 http://localhost:3004/
OK

1つの無限ループでは単純には停止しませんでした。

しかし、以下のように12個無限ループを起動すると……

carrier threadを枯渇させた場合
$ for i in $(seq 1 12); do curl http://localhost:3004/loop & done

$ curl -m 5 http://localhost:3004/
curl: (28) Operation timed out after 5001 milliseconds with 0 bytes received

一定量の無限ループ下では応答しなくなりました。

まとめ

言語/ランタイム 並行処理モデル 無限ループ時
Node.js イベントループ (シングルスレッド) 全停止
Python (asyncio) イベントループ (シングルスレッド) 全停止
Go goroutine (プリエンプティブ) 停止しない
Java (Virtual Thread) Virtual Thread (協調的) コア数分で停止

このシナリオにおいてはGoが最強ですね。

Rustは実験していませんが、tokioなどのasyncランタイムもワーカースレッドプールが固定サイズのため、Javaと似たような結果になると思われます。

方式ごとの特性は把握しておきたいものです。

  1. 監視スレッドによるシグナル送信&シグナルハンドラによる強制切替方式

14
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
14
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?