<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Worker Threads в Node.js: спасение от блокировки event loop на 10GB CSV и крипте]]></title><description><![CDATA[<p dir="auto">Event loop в Node.js - это святое. Один синхронный forEach по 10GB CSV или крипто-хэш по мегабайтам данных - и твой сервер висит, как легаси на IE6. Worker Threads решают эту проблему, вынося тяжёлые вычисления в отдельные потоки.</p>
<p dir="auto">Зачем это нужно? Потому что дефолтный thread pool из 4 потоков от libuv быстро кончается на CPU-intensive задачах. Парсинг огромных файлов или крипто-операции блочат не только event loop, но и worker pool. С Worker Threads ты спавнишь изолированные V8-инстансы с собственным event loop - и основной поток дышит свободно.</p>
<h2>Почему event loop блокируются даже на ‘асинхронных’ задачах</h2>
<p dir="auto">Node.js кажется асинхронным раем, но под капотом прячется libuv с фиксированным пулом из 4 worker threads. Эти ребята берут на себя блокирующий I/O: fs.readFileSync, crypto.pbkdf2, zlib deflate - всё, что C++ код делает синхронно. Но если твой JavaScript-код тяжёлый - цикл по 10GB CSV с парсингом строк или миллион хэшей - он жрёт один из этих 4 потоков.</p>
<p dir="auto">Представь: у тебя API для парсинга отчётов. Клиент кидает 10GB CSV, ты запускаешь sync-парсер - event loop встал, ответы не летят, 502 ошибки сыплются. То же с крипто: pbkdf2 на 1M итераций по массиву ключей. Thread pool забит, новые запросы в очередь. А если нагрузка - кластер из 8 ядер, каждый воркер спавнит подзадачи, и GC улетает на 70% CPU от кросс-трэд утечек.</p>
<ul>
<li><strong>Блокировка event loop</strong>: Длинный JS-цикл (&gt;50мс) фризит callbacks, throughput падает до 0.</li>
<li><strong>Worker pool исчерпан</strong>: 4 потока на все DNS, crypto, file I/O - одна тяжёлая задача монополизирует.</li>
<li><strong>Кластерные грабли</strong>: Воркеры Node + worker_threads = double GC на SharedArrayBuffer, утечки в postMessage.</li>
</ul>
<table class="table table-bordered table-striped">
<thead>
<tr>
<th>Проблема</th>
<th>Без Worker Threads</th>
<th>С Worker Threads</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>10GB CSV парсинг</strong></td>
<td>Event loop фриз на 30с+</td>
<td>Основной поток свободен, прогресс по сообщениям</td>
</tr>
<tr>
<td><strong>Крипто 1M ключей</strong></td>
<td>Thread pool забит, новые API висят</td>
<td>Параллельный поток, общий результат по shared memory</td>
</tr>
<tr>
<td><strong>CPU load</strong></td>
<td>100% на main thread</td>
<td>Распределено, GC не взлетает</td>
</tr>
</tbody>
</table>
<h2>Worker Threads под капотом: не child_process, а лёгкие братья</h2>
<p dir="auto">Worker Threads - это не форк процессов, как child_process. Каждый worker - отдельный V8 изолят с event loop, но в одном OS-процессе. Память шейрится через SharedArrayBuffer + Atomics, данные передаются копированием или transfer. Спавн лёгкий: 10-20мс vs 100мс+ на child_process.</p>
<p dir="auto">В одном файле описываешь и main, и worker: if (isMainThread) { … } else { … }. Общаешься через parentPort.postMessage(). Worker умирает сам, когда код кончился и нет таймеров. Но <em>нюанс</em>: копирование больших объектов - overhead, используй transferable objects для ArrayBuffer.</p>
<ul>
<li><strong>Создание</strong>: new Worker(__filename, { workerData: bigArray.buffer })</li>
<li><strong>Shared memory</strong>: new SharedArrayBuffer(size), Atomics для синхронизации</li>
<li><strong>Lifecycle</strong>: worker.on(‘message’), worker.on(‘exit’) - чистый ресурсный менеджмент</li>
<li><strong>isMainThread</strong>: API для проверки контекста в одном файле</li>
</ul>
<p dir="auto">Вот пример для крипто:</p>
<pre><code class="language-js">const { Worker, isMainThread, parentPort, workerData } = require('worker_threads');

if (isMainThread) {
  const worker = new Worker(__filename, { workerData: { keys: arrayOfKeys } });
  worker.on('message', (result) =&gt; console.log('Готово:', result));
} else {
  const hashes = workerData.keys.map(key =&gt; crypto.pbkdf2Sync(...));
  parentPort.postMessage(hashes);
}
</code></pre>
<h2>Парсинг 10GB CSV без смерти сервера</h2>
<p dir="auto">10GB файл - это не шутки: sync чтение через fs.createReadStream с парсингом строк в цикле забьёт event loop на минуты. Worker берёт поток на себя: читает чанками, парсит, отправляет прогресс. Основной поток отвечает клиенту ‘Обработка запущена’ и слушает обновления.</p>
<p dir="auto"><em>Важно</em>: не передавай весь файл в workerData - копирование убьёт память. Передавай путь к файлу строкой, пусть worker сам fs.open. Для прогресса шлите { progress: 0.47 } каждые 100MB. На выходе - агрегированный JSON или вставка в DB батчами.</p>
<ol>
<li><strong>Чанкинг</strong>: fs.createReadStream в worker, парсинг по строкам с csv-parser.</li>
<li><strong>Прогресс</strong>: parentPort.postMessage({ loaded: bytesRead / totalSize })</li>
<li><strong>Результат</strong>: Собранный объект или transfer ArrayBuffer для больших данных.</li>
<li><strong>Error handling</strong>: worker.on(‘error’) в main, terminate() если таймаут.</li>
</ol>
<p dir="auto">Таблица сравнения для 10GB CSV:</p>
<table class="table table-bordered table-striped">
<thead>
<tr>
<th>Подход</th>
<th>Время обработки</th>
<th>Event loop блок?</th>
<th>Память</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Sync в main</strong></td>
<td>5мин</td>
<td>Да, полный DoS</td>
<td>2GB+</td>
</tr>
<tr>
<td><strong>Libuv pool</strong></td>
<td>4мин</td>
<td>Частично, queue</td>
<td>1.5GB</td>
</tr>
<tr>
<td><strong>Worker Thread</strong></td>
<td>3мин</td>
<td>Нет</td>
<td>1GB (shared)</td>
</tr>
</tbody>
</table>
<h2>Крипто-операции: pbkdf2 и SHA не для main thread</h2>
<p dir="auto">Crypto в Node - CPU-киллер: pbkdf2Sync(1M итераций) на 10k ключах - секунды фриза. Worker Threads идеальны: весь crypto.createHash, scrypt, pbkdf2 в изоляте. SharedArrayBuffer для ключей - Atomics.add для счётчика прогресса.</p>
<p dir="auto">Грабли: postMessage копирует объекты, не уничтожай оригиналы - утечка. Используй worker.terminate() после finish. В кластере - по 1-2 worker на core, не больше, иначе GC overhead.</p>
<ul>
<li><strong>Батчинг</strong>: Разбей на 1k ключей за раз, message по батчам.</li>
<li><strong>Transferable</strong>: keys.buffer для zero-copy передачи.</li>
<li><strong>Параллелизм</strong>: Несколько workers на multi-core, load balance по CPU.</li>
</ul>
<pre><code class="language-js">// В worker
const { salt, iterations } = workerData;
const result = [];
for (let i = 0; i &lt; 10000; i++) {
  result.push(crypto.pbkdf2Sync(passwords[i], salt, iterations, 64, 'sha512'));
}
parentPort.postMessage(result.map(h =&gt; h.toString('hex')));
</code></pre>
<h2>Когда Worker Threads бьют по производительности</h2>
<p dir="auto">Worker Threads - не панацея. Overhead на спавн V8 (50-100мс), копирование данных, GC на shared memory. На мелких задачах (&lt;100мс) - хуже sync. В кластере с pm2 - следи за nested threads, иначе 70% CPU на double-marking.</p>
<p dir="auto">Остаётся подумать над гибридами: кластер + workers только для пиковых нагрузок. Или мигрируй на Bun/ Deno с native многопоточностью - но это уже другой подкапот. Тестируй под нагрузкой: 10GB CSV + 100 RPS API, увидишь реальные цифры.</p>
]]></description><link>https://forum.exlends.com/topic/2180/worker-threads-v-node.js-spasenie-ot-blokirovki-event-loop-na-10gb-csv-i-kripte</link><generator>RSS for Node</generator><lastBuildDate>Fri, 24 Apr 2026 14:08:48 GMT</lastBuildDate><atom:link href="https://forum.exlends.com/topic/2180.rss" rel="self" type="application/rss+xml"/><pubDate>Fri, 24 Apr 2026 08:15:42 GMT</pubDate><ttl>60</ttl></channel></rss>