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

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

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

Начать курс

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

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

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

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

Рассмотрим простой пример асинхронного кода:

async function fetchData(url: string): Promise<string> {
    const response = await fetch(url);
    return response.text();
}

async function main(): Promise<void> {
    console.log('Начало загрузки');
    const data = await fetchData('https://api.example.com/data');
    console.log('Данные получены:', data);
}

main();
console.log('Код продолжает выполняться');

В этом примере await не блокирует основной поток. Пока ожидается ответ от сервера, Event Loop продолжает обрабатывать другие задачи, такие как выполнение console.log. Это и есть конкурентность — способность обрабатывать несколько задач за один промежуток времени, переключаясь между ними.

2. Промисы и async/await: управление асинхронными операциями

TypeScript предоставляет удобные синтаксические конструкции для работы с асинхронностью. Промисы (Promises) — это объекты, представляющие будущий результат асинхронной операции. Комбинация async/await делает код линейным и читаемым.

Рассмотрим более сложный сценарий — параллельное выполнение нескольких запросов:

async function loadMultipleUsers(userIds: number[]): Promise<User[]> {
    const promises = userIds.map(id => fetch(`/users/${id}`).then(res => res.json()));
    return Promise.all(promises);
}

// Использование
const users = await loadMultipleUsers([1, 2, 3, 4, 5]);
console.log(users); // Все пользователи загружены параллельно

Метод Promise.all запускает все промисы одновременно, что значительно ускоряет выполнение по сравнению с последовательной загрузкой. Это идеальный пример конкурентности: задачи выполняются «одновременно» в рамках одного потока.

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

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

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

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

function factorial(n: number): number {
    if (n === 0) return 1;
    return n * factorial(n - 1);
}

parentPort?.on('message', (n: number) => {
    const result = factorial(n);
    parentPort?.postMessage(result);
});

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

function runWorker(n: number): Promise<number> {
    return new Promise((resolve, reject) => {
        const worker = new Worker('./worker.ts');
        worker.postMessage(n);
        worker.on('message', resolve);
        worker.on('error', reject);
    });
}

async function main() {
    const result = await runWorker(20);
    console.log('Факториал 20:', result);
}

main();

Каждый Worker работает в собственном потоке, имеет свой Event Loop и изолированную память. Это позволяет загружать все ядра процессора, не блокируя основной поток приложения.

4. Практические рекомендации и лучшие практики

При работе с конкурентностью и многопоточностью в TypeScript важно соблюдать несколько правил:

  • Используйте асинхронность для I/O — файловая система, базы данных, HTTP-запросы. Для этого достаточно async/await.
  • Применяйте Worker Threads для CPU — если задача нагружает процессор дольше 50-100 мс, выносите её в отдельный поток.
  • Избегайте гонок данных — при работе с общими ресурсами используйте Atomics или передавайте данные через сообщения.
  • Контролируйте количество воркеров — оптимальное число обычно равно количеству ядер процессора (доступно через os.cpus().length).
  • Типизируйте сообщения — TypeScript позволяет строго определить типы данных, передаваемых между потоками.

Пример типизированного сообщения:

interface TaskMessage {
    type: 'calculate' | 'process';
    data: unknown;
}

interface ResultMessage {
    success: boolean;
    result: unknown;
}

// В воркере
parentPort?.on('message', (msg: TaskMessage) => {
    // Обработка с гарантией типа
});

Заключение

Конкурентность и многопоточность в TypeScript — это не просто теория, а практические инструменты для создания быстрых и отзывчивых приложений. Асинхронность на основе Event Loop отлично подходит для I/O-задач, а Worker Threads дают настоящую многопоточность для вычислительных операций. Комбинируя эти подходы, вы сможете максимально эффективно использовать ресурсы сервера и обеспечить высокую производительность вашего кода.

Начните с малого: оптимизируйте асинхронные запросы с помощью Promise.all, а затем, при необходимости, добавьте Worker Threads для тяжелых вычислений. TypeScript с его строгой типизацией сделает этот процесс безопасным и предсказуемым.

Блоги

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