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

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

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

Начать курс

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

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

1. Фундаментальные основы: Event Loop и async/await

Прежде чем погружаться в многопоточность, важно понимать, как работает Event Loop в TypeScript. Конкурентность в однопоточной среде достигается за счет неблокирующего ввода-вывода и асинхронных операций.

Рассмотрим пример с использованием Promise.allSettled() для конкурентного выполнения нескольких асинхронных задач с обработкой ошибок:

interface TaskResult<T> {
  status: 'fulfilled' | 'rejected';
  value?: T;
  reason?: Error;
}

async function runConcurrentTasks<T>(
  tasks: Promise<T>[]
): Promise<TaskResult<T>[]> {
  const results = await Promise.allSettled(tasks);
  
  return results.map(result => {
    if (result.status === 'fulfilled') {
      return { status: 'fulfilled', value: result.value };
    } else {
      return { status: 'rejected', reason: result.reason };
    }
  });
}

// Использование
const task1 = fetchData('/api/users');
const task2 = fetchData('/api/posts');
const task3 = fetchData('/api/comments');

const results = await runConcurrentTasks([task1, task2, task3]);

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

Для CPU-интенсивных задач (например, обработка изображений, криптография, сложные вычисления) необходимы настоящие потоки. В Node.js для этого используются Worker Threads.

Создадим типизированный Worker с TypeScript:

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

interface WorkerInput {
  data: number[];
  operation: 'sum' | 'multiply';
}

interface WorkerOutput {
  result: number;
  duration: number;
}

if (parentPort) {
  const input: WorkerInput = workerData;
  const start = performance.now();
  
  let result: number;
  
  if (input.operation === 'sum') {
    result = input.data.reduce((acc, val) => acc + val, 0);
  } else {
    result = input.data.reduce((acc, val) => acc * val, 1);
  }
  
  const output: WorkerOutput = {
    result,
    duration: performance.now() - start
  };
  
  parentPort.postMessage(output);
}

Теперь создадим менеджер для управления несколькими Worker'ами:

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

class ThreadPool<TInput, TOutput> {
  private workers: Worker[] = [];
  private taskQueue: Array<{
    input: TInput;
    resolve: (value: TOutput) => void;
    reject: (reason?: any) => void;
  }> = [];

  constructor(
    private workerScript: string,
    private poolSize: number = 4
  ) {
    this.initialize();
  }

  private initialize(): void {
    for (let i = 0; i < this.poolSize; i++) {
      const worker = new Worker(this.workerScript);
      
      worker.on('message', (result: TOutput) => {
        const task = this.taskQueue.shift();
        if (task) {
          task.resolve(result);
        }
        worker.postMessage(this.taskQueue[0]?.input);
      });

      worker.on('error', (error) => {
        const task = this.taskQueue.shift();
        if (task) {
          task.reject(error);
        }
      });

      this.workers.push(worker);
    }
  }

  async execute(input: TInput): Promise<TOutput> {
    return new Promise((resolve, reject) => {
      this.taskQueue.push({ input, resolve, reject });
      
      const availableWorker = this.workers.find(
        w => w.threadId !== undefined
      );
      
      if (availableWorker) {
        availableWorker.postMessage(input);
      }
    });
  }

  async terminate(): Promise<void> {
    await Promise.all(
      this.workers.map(worker => worker.terminate())
    );
  }
}

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

Для максимальной производительности можно использовать разделяемую память между потоками с помощью SharedArrayBuffer и атомарных операций Atomics.

// sharedMemory.ts
interface SharedCounter {
  buffer: SharedArrayBuffer;
  counter: Int32Array;
}

function createSharedCounter(initialValue: number = 0): SharedCounter {
  const buffer = new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT);
  const counter = new Int32Array(buffer);
  counter[0] = initialValue;
  return { buffer, counter };
}

class AtomicCounter {
  private counter: Int32Array;

  constructor(shared: SharedCounter) {
    this.counter = shared.counter;
  }

  increment(): number {
    return Atomics.add(this.counter, 0, 1);
  }

  decrement(): number {
    return Atomics.sub(this.counter, 0, 1);
  }

  getValue(): number {
    return Atomics.load(this.counter, 0);
  }

  compareAndSwap(expected: number, newValue: number): boolean {
    const result = Atomics.compareExchange(
      this.counter, 0, expected, newValue
    );
    return result === expected;
  }
}

// Пример использования в Worker
// worker.ts
import { parentPort, workerData } from 'worker_threads';

const shared = workerData as SharedCounter;
const counter = new AtomicCounter(shared);

// Безопасное обновление счетчика
const oldValue = counter.increment();
parentPort?.postMessage({ oldValue, newValue: oldValue + 1 });

4. Продвинутые паттерны синхронизации

При работе с многопоточностью важны паттерны синхронизации. Рассмотрим реализацию мьютекса с использованием Atomics:

class Mutex {
  private locked: Int32Array;
  private static UNLOCKED = 0;
  private static LOCKED = 1;

  constructor(sharedBuffer?: SharedArrayBuffer) {
    if (sharedBuffer) {
      this.locked = new Int32Array(sharedBuffer);
    } else {
      const buffer = new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT);
      this.locked = new Int32Array(buffer);
      this.locked[0] = Mutex.UNLOCKED;
    }
  }

  async lock(): Promise<void> {
    while (true) {
      const oldValue = Atomics.compareExchange(
        this.locked,
        0,
        Mutex.UNLOCKED,
        Mutex.LOCKED
      );
      
      if (oldValue === Mutex.UNLOCKED) {
        return;
      }
      
      // Ожидание с yield для эффективности
      await new Promise(resolve => setTimeout(res                

Блоги

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