← Назад к вопросам

Почему Node.JS считается однопоточным ЯП?

2.0 Middle🔥 251 комментариев
#Node.js и JavaScript

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI29 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Почему Node.js считается однопоточным языком программирования

Это частый вопрос на интервью, и ответ неочевидный, потому что Node.js НА САМОМ ДЕЛЕ не полностью однопоточный. Давайте разберёмся.

Что имеют в виду под "однопоточный"

JavaScript код выполняется в одном потоке (Main Thread):

// Этот код выполняется в ОДНОМ потоке
console.log('Start');

setTimeout(() => {
  console.log('Middle');  // Выполнится в том же потоке
}, 1000);

console.log('End');

// Вывод:
// Start
// End
// Middle (через 1 секунду)

Нет параллельного выполнения:

// ❌ Это НЕ возможно:
thread1: sum = 0;
thread2: sum = 1;  // Параллельно в другом потоке
// Результат не определён

// ✅ В Node.js всегда упорядоченно:
sum = 0;
sum = 1;
// Всегда в одном потоке, всегда предсказуемо

Архитектура Node.js

┌─────────────────────────────────────┐
│   JavaScript Code (Main Thread)     │
│                                     │
│  console.log('Hello');              │
│  const data = fs.readFileSync();    │
│  process.nextTick(() => {});        │
└────────────┬────────────────────────┘
             │
      ┌──────▼──────────────────┐
      │   Event Loop (1 thread) │
      │                         │
      │  Checks for:            │
      │  - Timers               │
      │  - Callbacks            │
      │  - I/O operations       │
      │  - Microtasks           │
      └──────┬──────────────────┘
             │
      ┌──────▼──────────────────┐
      │  libuv Thread Pool      │
      │  (4-256 threads)        │
      │                         │
      │  Runs async operations: │
      │  - File I/O             │
      │  - DNS lookups          │
      │  - Crypto               │
      │  - Network operations   │
      └─────────────────────────┘

Главный момент: JavaScript код ВСЕГДА выполняется в ОДНОМ потоке, но I/O операции могут выполняться в фоновом thread pool.

Демонстрация

Main Thread (JavaScript):

const fs = require('fs');
const crypto = require('crypto');

// Это выполнится в ГЛАВНОМ потоке
console.log('1. Starting');

// Это выполнится в фоновом потоке libuv
fs.readFile('file.txt', (err, data) => {
  console.log('3. File read');
});

// Это тоже в фоновом потоке
crypto.pbkdf2('password', 'salt', 100000, 64, 'sha512', (err, hash) => {
  console.log('4. Hash computed');
});

// Это в ГЛАВНОМ потоке
console.log('2. All async operations started');

// Вывод:
// 1. Starting
// 2. All async operations started
// 3. File read (из фонового потока)
// 4. Hash computed (из фонового потока)

Проверка потоков:

const { Worker } = require('worker_threads');
const { threadId } = require('worker_threads');

console.log('Main thread ID:', threadId);  // 1

const worker = new Worker('./worker.js');
worker.on('message', (message) => {
  console.log('From worker:', message);
  // From worker: Worker thread ID: 2
});

Почему это "однопоточный"

Причина 1: Изоляция JavaScript кода

// JavaScript всегда выполняется в одном потоке
let counter = 0;

function increment() {
  counter++;  // Этот доступ безопасен, нет race conditions
}

increment();
increment();

console.log(counter);  // Всегда 2

В многопоточных языках (Java, C++):

// Java (многопоточный)
int counter = 0;

// Thread 1:
counter++;  // Может быть 1

// Thread 2 (параллельно):
counter++;  // Может быть 1 или 2? Undefined!

Причина 2: Упрощённая модель программирования

// В Node.js не нужны синхронизация и locks
class Database {
  async query(sql) {
    // Не нужны mutex/locks
    // JavaScript код выполняется упорядоченно
    const result = await this.execute(sql);
    return result;
  }
}

// В Java/C++ нужны:
class Database {
  public synchronized List<Row> query(String sql) {
    // synchronized = lock = complexity
    // ...
  }
}

Причина 3: Явная асинхронность

// Node.js: асинхронность явная
fs.readFile('file.txt', (err, data) => {
  // Callback = контроль потока
});

// Java: асинхронность скрытая
Thread thread1 = new Thread(() -> {
  // Запуск нового потока неявный
});
thread1.start();

На самом деле: многопоточность ЕСТЬ

Но это скрыто за API:

// Ты вызываешь ОДНУ функцию
fs.readFile('file.txt', callback);

// Но внутри:
// 1. Main thread получает запрос
// 2. libuv берёт рабочий thread из pool
// 3. Рабочий thread читает файл (параллельно!)
// 4. Результат отправляется обратно в main thread
// 5. Callback выполняется в main thread

Пример: несколько операций параллельно

const fs = require('fs');

// Все три операции выполняются ПАРАЛЛЕЛЬНО
// в разных потоках thread pool!
fs.readFile('file1.txt', (err, data) => {
  console.log('File 1 read');
});

fs.readFile('file2.txt', (err, data) => {
  console.log('File 2 read');
});

fs.readFile('file3.txt', (err, data) => {
  console.log('File 3 read');
});

console.log('All operations started');

// Вывод:
// All operations started
// File 1 read    <- все параллельно!
// File 2 read
// File 3 read

Worker Threads: когда нужна真настоящая многопоточность

Для CPU-intensive операций:

const { Worker } = require('worker_threads');
const path = require('path');

// Main thread
const worker = new Worker(path.join(__dirname, 'worker.js'));

// Отправляем работу в другой thread
worker.postMessage({ n: 1000000000 });

// Main thread может обрабатывать запросы
setInterval(() => {
  console.log('Main thread still responsive');
}, 1000);

// Результат из worker thread
worker.on('message', (result) => {
  console.log('Computation result:', result);
});

// worker.js
const { parentPort } = require('worker_threads');

parentPort.on('message', (message) => {
  let sum = 0;
  for (let i = 0; i < message.n; i++) {
    sum += Math.sqrt(i);
  }
  parentPort.postMessage(sum);
});

Сравнение с другими языками

ЯзыкМодельСинтаксис
Node.jsОднопоточный + Event Loopasync/await
PythonОднопоточный (GIL)threading (ограничено)
JavaМногопоточныйsynchronized, locks
RustМногопоточныйChannels, Arc<Mutex<T>>
GoМногопоточный (goroutines)go func(), channels

Плюсы "однопоточности"

✅ Нет race conditions
let counter = 0;
function increment() { counter++; }  // Всегда безопасно

✅ Нет deadlocks
// В Java: thread1 ждёт lock от thread2
//         thread2 ждёт lock от thread1
//         deadlock!

✅ Проще рассуждать о коде
for (const i of range(1000000)) {
  // Никто не изменит counter параллельно
}

✅ Асинхронность явная
fs.readFile()  // Явный асинхронный вызов
await db.query()  // Явно ждём результат

Минусы "однопоточности"

❌ Один медленный синхронный код блокирует ВСЁ
for (let i = 0; i < 1000000000; i++) {  // Ужас!
  Math.sqrt(i);
}
// Все пользователи ждут 10+ секунд

❌ Не можем использовать все CPU ядра (без cluster/PM2)
// Node.js использует только 1 ядро из 8
// Остальные 7 idleCPU-bound операции медленнее чем в Rust/Go
// JavaScript компилируется на лету (V8)
// Rust/Go имеют статическую компиляцию

Решение: масштабирование

Cluster Module:

const cluster = require('cluster');
const os = require('os');

if (cluster.isMaster) {
  // Мастер создаёт worker для каждого ядра
  for (let i = 0; i < os.cpus().length; i++) {
    cluster.fork();  // Новый процесс = новый Event Loop
  }
} else {
  // Worker
  http.createServer((req, res) => {
    res.end('Hello');
  }).listen(3000);
}

// Результат: используем ВСЕ ядра!

Вывод

Node.js "однопоточный" потому что:

  1. JavaScript код выполняется в одном потоке (Main Thread)

    • Нет race conditions
    • Нет deadlocks
    • Просто рассуждать
  2. Асинхронность скрыта в libuv

    • I/O операции в thread pool
    • Но JavaScript callback выполняется в main thread
  3. Это упрощает модель программирования

    • Не нужны locks и synchronized
    • Меньше bugs
    • Faster development

Но важно понимать:

  • Node.js НЕ монопоточный на уровне ОС
  • libuv использует thread pool для I/O
  • Для CPU-bound нужны Worker Threads
  • Для масштабирования нужен Cluster или PM2

Аналогия:

Отель с одним администратором (main thread)
Администратор не может одновременно
обслуживать двух гостей

Но работники (thread pool) могут готовить
документы параллельно

Когда гость приходит за документами,
администратор его обслуживает
(callback в main thread)

Это "однопоточная" модель, но с параллельными рабочими за сценой.

Почему Node.JS считается однопоточным ЯП? | PrepBro