Конкурентность и многопоточность в TypeScript: продвинутый уровень

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Конкурентность и многопоточность в TypeScript: продвинутый уровень

В современной веб-разработке и серверном программировании производительность и отзывчивость приложений критически важны. TypeScript, будучи надстройкой над JavaScript, унаследовал его однопоточную модель выполнения (Event Loop). Однако для решения ресурсоемких задач (обработка изображений, криптография, сложные вычисления) одной асинхронности недостаточно. На продвинутом уровне разработчику необходимо понимать, как организовать настоящую многопоточность и безопасную работу с разделяемой памятью. В этой статье мы разберем ключевые механизмы: Web Workers (браузер), Worker Threads (Node.js), SharedArrayBuffer и Atomics.

1. Web Workers: параллелизм в браузере

Web Workers — это основной способ запустить код в отдельном потоке в браузерной среде. Worker не имеет доступа к DOM, но может выполнять тяжелые вычисления, не блокируя основной поток (UI).

Создание Worker в TypeScript требует компиляции отдельного файла. Рассмотрим пример: вычисление больших чисел Фибоначчи.

Файл worker.ts (компилируется в worker.js):

// worker.ts
self.onmessage = (event: MessageEvent<number>) => {
  const n = event.data;
  const fib = (num: number): number => {
    if (num <= 1) return num;
    return fib(num - 1) + fib(num - 2);
  };
  const result = fib(n);
  self.postMessage(result);
};

Файл main.ts:

// main.ts
const worker = new Worker(new URL('./worker.js', import.meta.url));

worker.postMessage(40);

worker.onmessage = (event: MessageEvent<number>) => {
  console.log(`Результат: ${event.data}`); // Вывод после завершения
  worker.terminate();
};

worker.onerror = (error) => {
  console.error('Ошибка Worker:', error.message);
};

Важные нюансы:

  • Worker запускается в отдельном потоке, но не имеет доступа к глобальным переменным основного потока.
  • Передача данных происходит через копирование (структурное клонирование). Для больших данных используйте Transferable objects (например, ArrayBuffer).
  • Для TypeScript обязательно указывать типы в сообщениях, чтобы избежать ошибок времени выполнения.

2. Worker Threads: многопоточность в Node.js

Для серверной среды Node.js аналогом Web Workers является модуль worker_threads. Он позволяет создавать изолированные потоки с собственной памятью (V8 isolate). Это идеально для CPU-интенсивных операций, таких как хеширование или парсинг JSON.

Пример: создание Worker для хеширования пароля (worker.ts):

// crypto-worker.ts
import { parentPort, workerData } from 'worker_threads';
import { createHash } from 'crypto';

if (parentPort) {
  const { password, salt } = workerData;
  const hash = createHash('sha256')
    .update(password + salt)
    .digest('hex');
  parentPort.postMessage(hash);
}

Основной файл (main.ts):

// main.ts
import { Worker } from 'worker_threads';
import path from 'path';

function hashPassword(password: string, salt: string): Promise<string> {
  return new Promise((resolve, reject) => {
    const worker = new Worker(path.resolve(__dirname, 'crypto-worker.js'), {
      workerData: { password, salt }
    });

    worker.on('message', resolve);
    worker.on('error', reject);
    worker.on('exit', (code) => {
      if (code !== 0) reject(new Error(`Worker завершился с кодом ${code}`));
    });
  });
}

(async () => {
  const hash = await hashPassword('mySecret', 'randomSalt');
  console.log(`Хеш: ${hash}`);
})();

Ключевые отличия от Web Workers:

  • Работает в Node.js, доступ к файловой системе и процессу.
  • Передача данных через workerData при создании или через postMessage.
  • Поддерживает SharedArrayBuffer для разделяемой памяти (требуется флаг --enable-shared-memory).

3. SharedArrayBuffer и Atomics: разделяемая память

Обычная передача данных между потоками через postMessage создает копии, что накладно для больших объемов. SharedArrayBuffer позволяет нескольким потокам читать и писать в одну область памяти без копирования. Однако это требует синхронизации, которую обеспечивает объект Atomics.

Пример: синхронизированный счетчик с SharedArrayBuffer:

// shared-counter.ts
import { Worker } from 'worker_threads';
import { resolve } from 'path';

const buffer = new SharedArrayBuffer(4); // 4 байта для Int32
const counter = new Int32Array(buffer);

const workers: Worker[] = [];
for (let i = 0; i < 4; i++) {
  const worker = new Worker(resolve(__dirname, 'counter-worker.js'));
  worker.postMessage(buffer); // Передаем сам буфер
  workers.push(worker);
}

// Ждем завершения всех рабочих
await Promise.all(workers.map(w => new Promise(resolve => w.on('exit', resolve))));
console.log(`Итоговое значение: ${Atomics.load(counter, 0)}`); // 4000

Файл counter-worker.js:

// counter-worker.ts
import { parentPort } from 'worker_threads';

parentPort?.on('message', (buffer: SharedArrayBuffer) => {
  const counter = new Int32Array(buffer);
  for (let i = 0; i < 1000; i++) {
    // Атомарное прибавление
    Atomics.add(counter, 0, 1);
  }
  parentPort?.close();
});

Почему нужен Atomics? Без атомарных операций два потока могут одновременно прочитать значение, увеличить его и записать обратно, что приведет к потере одного инкремента (race condition). Atomics.add гарантирует, что операция чтения-модификации-записи выполняется как единое целое.

Основные методы Atomics:

  • Atomics.load(typedArray, index) — атомарное чтение.
  • Atomics.store(typedArray, index, value) — атомарная запись.
  • Atomics.add(typedArray, index, value) — атомарное сложение.

Блоги

Рекомендаций по книгам