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

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

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

Начать курс

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

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

Конкурентность vs Параллелизм: ключевые различия

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

  • Конкурентность — это способность программы обрабатывать несколько задач, переключаясь между ними. В JavaScript/TypeScript это реализовано через Event Loop и асинхронные операции.
  • Параллелизм — это одновременное выполнение нескольких задач на разных ядрах процессора. В Node.js это достигается через Worker Threads.

TypeScript (как и JavaScript) однопоточный по своей природе, но благодаря асинхронности и Worker Threads мы можем эффективно использовать оба подхода.

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

Асинхронное программирование — первый шаг к конкурентности. Рассмотрим основные механизмы.

Promises и async/await

Современный TypeScript активно использует async/await для работы с асинхронными операциями. Это позволяет писать код, который выглядит синхронным, но выполняется конкурентно.

// Пример асинхронной функции
async function fetchData(url: string): Promise<any> {
  const response = await fetch(url);
  return response.json();
}

// Конкурентный запуск нескольких запросов
async function fetchAllData(): Promise<void> {
  const [users, posts, comments] = await Promise.all([
    fetchData('/api/users'),
    fetchData('/api/posts'),
    fetchData('/api/comments')
  ]);
  
  console.log('Все данные загружены конкурентно');
}

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

Обработка ошибок в асинхронном коде

Важно правильно обрабатывать ошибки при конкурентном выполнении:

async function safeFetchAll(): Promise<void> {
  try {
    const results = await Promise.allSettled([
      fetchData('/api/users'),
      fetchData('/api/invalid'),
      fetchData('/api/posts')
    ]);
    
    results.forEach((result, index) => {
      if (result.status === 'fulfilled') {
        console.log(`Запрос ${index} успешен:`, result.value);
      } else {
        console.error(`Запрос ${index} провалился:`, result.reason);
      }
    });
  } catch (error) {
    console.error('Критическая ошибка:', error);
  }
}

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

Для CPU-интенсивных задач (обработка изображений, сложные вычисления) одной асинхронности недостаточно. Здесь на помощь приходят Worker Threads — настоящие потоки выполнения.

Создание Worker Thread

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

// 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 runFibonacci(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', (result: number) => {
      resolve(result);
      worker.terminate();
    });
    
    worker.on('error', reject);
    worker.on('exit', (code) => {
      if (code !== 0) {
        reject(new Error(`Worker stopped with exit code ${code}`));
      }
    });
  });
}

// Использование
async function main() {
  const results = await Promise.all([
    runFibonacci(40),
    runFibonacci(42),
    runFibonacci(44)
  ]);
  
  console.log('Результаты:', results);
}

main().catch(console.error);

Пул потоков для эффективной работы

Создавать новый Worker для каждой задачи неэффективно. Лучше использовать пул потоков:

// thread-pool.ts
import { Worker } from 'worker_threads';
import { EventEmitter } from 'events';

interface PoolTask {
  data: unknown;
  resolve: (value: unknown) => void;
  reject: (reason: unknown) => void;
}

export class ThreadPool extends EventEmitter {
  private workers: Worker[] = [];
  private queue: PoolTask[] = [];
  private activeWorkers = 0;

  constructor(private workerPath: string, private poolSize: number) {
    super();
    this.initializePool();
  }

  private initializePool(): void {
    for (let i = 0; i < this.poolSize; i++) {
      const worker = new Worker(this.workerPath);
      
      worker.on('message', (result) => {
        this.activeWorkers--;
        this.processQueue();
      });
      
      worker.on('error', (error) => {
        this.activeWorkers--;
        this.emit('error', error);
      });
      
      this.workers.push(worker);
    }
  }

  private processQueue(): void {
    if (this.queue.length > 0 && this.activeWorkers < this.poolSize) {
      const task = this.queue.shift()!;
      this.executeTask(task);
    }
  }

  private executeTask(task: PoolTask): void {
    const worker = this.workers[this.activeWorkers % this.poolSize];
    this.activeWorkers++;
    
    worker.postMessage(task.data);
    worker.once('message', (result) => {
      task.resolve(result);
    });
  }

  execute(data: unknown): Promise<unknown> {
    return new Promise((resolve, reject) => {
      const task: PoolTask = { data, resolve, reject };
      
      if (this.activeWorkers < this.poolSize) {
        this.executeTask(task);
      } else {
        this.queue.push(task);
      }
    });
  }

  terminate(): void {
    this.workers.forEach(worker => worker.terminate());
    this.workers = [];
  }
}

Event Loop: как Node.js управляет конкурентностью

Понимание Event Loop критически важно для эффективной работы с конкурентностью в TypeScript.

Фазы Event Loop

  • Timers — выполнение setTimeout/setInterval
  • I/O Callbacks — обработка завершенных операций ввода-вывода
  • Idle/Prepare — внутренние операции Node.js
  • Poll — получение новых I/O событий
  • Check — выполнение setImmediate
  • Close Callbacks — обработка закрытия соединений

Пример демонстрации порядка выполнения:

Блоги

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