Конкурентность в TypeScript: потоки, горутины и async/await — разбираемся на практике

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

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

Начать курс

Конкурентность в TypeScript: от Event Loop до Web Workers

Вы когда-нибудь замечали, как ваш браузер или Node.js-приложение «зависает» во время выполнения сложной математической операции или обработки большого JSON? Знакомое чувство бессилия, когда интерфейс перестаёт реагировать на клики, а сервер — на новые запросы. Всё дело в том, что по умолчанию JavaScript (и его строгий брат TypeScript) — однопоточный язык. Но это не значит, что мы обречены на медленные программы. Наоборот, грамотное использование конкурентности в TypeScript позволяет создавать быстрые и отзывчивые приложения.

В этой статье мы не просто перечислим термины. Мы разберём, как на самом деле работают async/await, что скрывается под капотом Event Loop, можно ли в TypeScript использовать «горутины» как в Go, и когда стоит применять настоящие потоки через Web Workers. Поехали!

1. Асинхронность vs Параллелизм: в чём разница?

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

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

TypeScript из коробки даёт нам асинхронность. Параллелизм же требует создания отдельных потоков — в браузере это Web Workers, в Node.js — worker_threads. Запомните эту разницу, иначе рискуете написать код, который будет «тормозить» из-за блокировки основного потока.

2. Event Loop и коллбеки — фундамент конкурентности TypeScript

Главный секрет асинхронности в JavaScript/TypeScript — это Event Loop (цикл событий). Представьте, что у вас есть очередь задач. Основной поток берёт задачу, выполняет её, если она синхронная — сразу выдаёт результат. Если же это асинхронная операция (например, setTimeout или fetch), то она отправляется в «сторону» (в Web API или libuv), а основной поток продолжает выполнять следующий код.

Когда асинхронная операция завершается, её коллбек попадает в очередь (Task Queue или Microtask Queue). Event Loop постоянно проверяет, не пуст ли стек вызовов, и если он пуст — берёт первую задачу из очереди и выполняет.

// Простой пример, который это демонстрирует
console.log('1. Начало');

setTimeout(() => {
    console.log('3. Таймер сработал');
}, 0);

Promise.resolve().then(() => {
    console.log('2. Микротаска выполнена');
});

console.log('4. Конец');

// Вывод: 1, 4, 2, 3

Обратите внимание: Promise (микротаска) выполнился раньше setTimeout (макротаска), хотя задержка у таймера 0 мс. Это важное знание для оптимизации TypeScript.

3. Async/Await — сахар, который меняет всё

С появлением ES2017 мир JS разделился на «до» и «после». Async/await — это синтаксический сахар над промисами, который делает асинхронный код похожим на синхронный. Он не добавляет потоков, но радикально повышает читаемость.

// Функция, симулирующая задержку
function delay(ms: number): Promise<void> {
    return new Promise(resolve => setTimeout(resolve, ms));
}

// Асинхронная функция с использованием async/await
async function fetchUserData(userId: number): Promise<string> {
    console.log(`Запрашиваю данные пользователя ${userId}...`);
    await delay(2000); // Имитация сетевого запроса
    return `Данные пользователя ${userId}`;
}

async function main() {
    console.log('Старт');
    const user1 = await fetchUserData(1);
    console.log(user1);
    const user2 = await fetchUserData(2);
    console.log(user2);
    console.log('Финиш');
}

main();
// Выполнение: Старт -> пауза 2с -> данные 1 -> пауза 2с -> данные 2 -> Финиш

Это последовательное выполнение. А что если мы хотим запустить оба запроса параллельно? Для этого конкурентность в TypeScript предлагает использовать Promise.all:

async function mainParallel() {
    console.log('Старт параллельного выполнения');
    const results = await Promise.all([
        fetchUserData(1),
        fetchUserData(2)
    ]);
    console.log(results); // ['Данные пользователя 1', 'Данные пользователя 2']
    console.log('Финиш');
}

mainParallel();
// Выполнение: Старт -> пауза 2с -> результат -> Финиш

Оба запроса запускаются почти одновременно, и общее время выполнения сокращается с 4 до 2 секунд. Это и есть истинная мощь асинхронного программирования TypeScript.

4. Горутины в TypeScript: миф или реальность?

Если вы пришли из мира Go, вы наверняка скучаете по лёгким горутинам. Напомним: горутины — это очень дешёвые (весом в несколько килобайт) потоки выполнения, управляемые рантаймом Go. В TypeScript нет встроенных горутин, но их концепцию можно эмулировать.

В Node.js и браузерах роль «лёгких потоков» выполняют микротаски (промисы). Однако есть важное отличие: горутины в Go могут выполняться параллельно на разных ядрах, а промисы — нет. Для настоящих «горутин» в TypeScript придётся использовать Web Workers или worker_threads.

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

// Эмуляция горутин через массив промисов
const tasks: Promise<void>[] = [];
for (let i = 0; i < 10; i++) {
    tasks.push(
        delay(Math.random() * 1000).then(() => console.log(`Задача ${i} завершена`))
    );
}
await Promise.all(tasks);
console.log('Все задачи выполнены');

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

5. Потоки в TypeScript: Web Workers и worker_threads

Когда вам нужен настоящий параллелизм — например, для обработки изображений, сложных вычислений или парсинга больших файлов без блокировки UI — на помощь приходят потоки.

Web Workers в браузере

Web Worker — это скрипт, который выполняется в отдельном потоке операционной системы. Он не имеет доступа к DOM, но может обмени

Блоги

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