Конкурентность и многопоточность в 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 — обработка закрытия соединений
Пример демонстрации порядка выполнения: