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

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

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

Начать курс

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

Современные веб-приложения всё чаще сталкиваются с необходимостью обработки больших объёмов данных и выполнения ресурсоёмких операций без блокировки пользовательского интерфейса. TypeScript, будучи надстройкой над JavaScript, наследует его однопоточную модель выполнения, но при этом предоставляет мощные инструменты для организации конкурентности и многопоточности. В этой статье мы разберём, как эффективно использовать эти возможности в ваших проектах.

Понимание конкурентности и многопоточности

Прежде чем углубляться в код, важно разграничить два ключевых понятия:

  • Конкурентность — способность системы обрабатывать несколько задач в перекрывающиеся промежутки времени, но не обязательно одновременно. Это достигается за счёт быстрого переключения контекста.
  • Многопоточность — параллельное выполнение нескольких потоков на разных ядрах процессора. В Node.js это реализуется через Worker Threads.

TypeScript работает в среде Node.js, где основной поток обрабатывает события через Event Loop. Для тяжёлых вычислений это может стать узким местом.

Асинхронность и Promise: основа конкурентности

Базовый уровень конкурентности в TypeScript строится на async/await и Promise. Эти механизмы позволяют не блокировать основной поток во время ожидания I/O операций.

// Пример асинхронной загрузки данных
interface User {
  id: number;
  name: string;
}

async function fetchUser(id: number): Promise<User> {
  const response = await fetch(`https://api.example.com/users/${id}`);
  return response.json();
}

// Параллельный запуск нескольких запросов
async function loadUsers(): Promise<User[]> {
  const userIds = [1, 2, 3, 4, 5];
  const promises = userIds.map(id => fetchUser(id));
  return Promise.all(promises);
}

loadUsers().then(users => {
  console.log('Загружено пользователей:', users.length);
});

Promise.all запускает все промисы одновременно, но они выполняются в одном потоке. Это идеально для I/O-задач, но не подходит для CPU-интенсивных операций.

Worker Threads: настоящая многопоточность

Для выполнения тяжёлых вычислений без блокировки основного потока используйте Worker Threads. Это встроенный модуль Node.js, который создаёт отдельные потоки с собственной памятью.

Создадим пример вычисления чисел Фибоначчи в отдельном потоке:

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

function fibonacci(n: number): number {
  if (n <= 1) return n;
  return fibonacci(n - 1) + fibonacci(n - 2);
}

parentPort?.on('message', (n: number) => {
  const result = fibonacci(n);
  parentPort?.postMessage(result);
});
// main.ts
import { Worker } from 'worker_threads';
import path from 'path';

function runFibonacciInWorker(n: number): Promise<number> {
  return new Promise((resolve, reject) => {
    const worker = new Worker(path.resolve(__dirname, 'fibonacci.worker.ts'));
    
    worker.postMessage(n);
    worker.on('message', resolve);
    worker.on('error', reject);
    worker.on('exit', (code) => {
      if (code !== 0) reject(new Error(`Worker stopped with exit code ${code}`));
    });
  });
}

async function main() {
  console.time('fibonacci');
  const result = await runFibonacciInWorker(40);
  console.timeEnd('fibonacci');
  console.log(`Fibonacci(40) = ${result}`);
}

main();

Обратите внимание: для запуска TypeScript-воркеров потребуется компиляция в JavaScript. Можно использовать ts-node или предварительную компиляцию.

SharedArrayBuffer: разделяемая память между потоками

Когда потокам нужно обмениваться большими объёмами данных, копирование через postMessage может быть неэффективным. SharedArrayBuffer позволяет потокам работать с общей областью памяти.

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

parentPort?.on('message', (buffer: SharedArrayBuffer) => {
  const view = new Int32Array(buffer);
  // Модифицируем данные в разделяемой памяти
  for (let i = 0; i < view.length; i++) {
    view[i] = view[i] * 2;
  }
  parentPort?.postMessage('done');
});
// main.ts
import { Worker } from 'worker_threads';
import path from 'path';

const buffer = new SharedArrayBuffer(1024);
const view = new Int32Array(buffer);

// Заполняем массив данными
for (let i = 0; i < view.length; i++) {
  view[i] = i;
}

const worker = new Worker(path.resolve(__dirname, 'shared.worker.ts'));
worker.postMessage(buffer);
worker.on('message', () => {
  console.log('Модифицированные данные:', view.slice(0, 10));
  worker.terminate();
});

При работе с SharedArrayBuffer обязательно используйте Atomics для синхронизации доступа, чтобы избежать состояния гонки:

// Безопасная запись с использованием Atomics
Atomics.store(view, 0, 42);
const value = Atomics.load(view, 0);
console.log(value); // 42

Пул воркеров для эффективного управления ресурсами

Создание нового воркера для каждой задачи — дорогостоящая операция. Лучше использовать пул воркеров, который переиспользует потоки:

import { Worker } from 'worker_threads';
import path from 'path';

class WorkerPool {
  private workers: Worker[] = [];
  private queue: Array<{ data: unknown; resolve: Function; reject: Function }> = [];

  constructor(private size: number, private workerPath: string) {
    for (let i = 0; i < size; i++) {
      this.workers.push(this.createWorker());
    }
  }

  private createWorker(): Worker {
    const worker = new Worker(path.resolve(__dirname, this.workerPath));
    worker.on('message', (result) => {
      const task = this.queue.shift();
      task?.resolve(result);
    });
    worker.on('error', (err) => {
      const task = this.queue.shift();
      task?.reject(err);
    });
    return worker;
  }

  execute(data: unknown): Promise<unknown> {
    return new Promise((resolve, reject) => {
      this.queue.push({ data, resolve, reject });
      const worker = this.workers[this.queue.length % this.size];
      worker.postMessage(data);
    });
  }

  terminate(): void {
    this.workers.forEach(w => w.terminate());
  }
}

// Использование
const pool = new WorkerPool(4, 'fibonacci.worker.ts');
const                

Блоги

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