Tính Đồng thời và Song song trong TypeScript

Trong phát triển phần mềm hiện đại, hiệu suất cao và khả năng phản hồi nhanh là các yêu cầu thiết yếu. Điều này khiến các nhà phát triển phải nắm vững cách xử lý đồng thời và song song.

Tính Đồng thời và Song song trong TypeScript

Phân biệt Xử lý Đồng thời và Song song

Trước khi đi vào chi tiết về lập trình, cần hiểu rõ hai khái niệm này:

1. Xử lý Đồng thời (Concurrency):

Định nghĩa: Khả năng thực hiện nhiều tác vụ xen kẽ, nhưng không nhất thiết phải cùng lúc.

Ví dụ thực tế: Một đầu bếp đa nhiệm, chuyển qua lại giữa nhiều món ăn.

2. Xử lý Song song (Parallelism):

Định nghĩa: Thực hiện nhiều tác vụ đồng thời bằng cách sử dụng nhiều tài nguyên xử lý (như CPU đa lõi).

Ví dụ thực tế: Nhiều đầu bếp làm việc trên các món ăn khác nhau cùng lúc.


Xử lý Đồng thời trong TypeScript

JavaScript (và TypeScript) sử dụng mô hình đơn luồng (single-threaded) với vòng lặp sự kiện. Tuy nhiên, xử lý đồng thời có thể đạt được thông qua lập trình bất đồng bộ với callbacks, promises, và async/await.

  1. Promises cho Xử lý Đồng thời
const fetchData = (url: string): Promise<string> => {
  return new Promise((resolve) => {
    setTimeout(() => resolve(`Data from ${url}`), 1000);
  });
};

const main = async () => {
  console.log('Fetching data concurrently...');
  const data1 = fetchData('https://api.example.com/1');
  const data2 = fetchData('https://api.example.com/2');
  
  const results = await Promise.all([data1, data2]);
  console.log(results); // ["Data from https://api.example.com/1", "Data from https://api.example.com/2"]
};
main();

Giải thích:

Hàm Promise.all cho phép thực thi đồng thời nhiều promise, tiết kiệm thời gian xử lý.

  1. Sử dụng Async/Await
async function task1() {
  console.log("Task 1 started");
  await new Promise((resolve) => setTimeout(resolve, 2000));
  console.log("Task 1 completed");
}

async function task2() {
  console.log("Task 2 started");
  await new Promise((resolve) => setTimeout(resolve, 1000));
  console.log("Task 2 completed");
}

async function main() {
  console.log("Concurrent execution...");
  await Promise.all([task1(), task2()]);
  console.log("All tasks completed");
}
main();

Giải thích:

Async/Await giúp mã dễ đọc hơn trong khi vẫn duy trì tính không đồng bộ.


Xử lý Song song trong TypeScript

Mặc dù JavaScript không hỗ trợ đa luồng nguyên bản, nhưng Web WorkersNode.js Worker Threads cung cấp cơ chế để thực thi song song.

  1. Web Workers

Dùng trong trình duyệt để chạy các tác vụ trên luồng riêng biệt.

// worker.ts
addEventListener('message', (event) => {
  const result = event.data.map((num: number) => num * 2);
  postMessage(result);
});

// main.ts
const worker = new Worker('worker.js');

worker.onmessage = (event) => {
  console.log('Result from worker:', event.data);
};

worker.postMessage([1, 2, 3, 4]);
  1. Worker Threads trong Node.js
// worker.js
const { parentPort } = require('worker_threads');

parentPort.on('message', (data) => {
  const result = data.map((num) => num * 2);
  parentPort.postMessage(result);
});

// main.js
const { Worker } = require('worker_threads');

const worker = new Worker('./worker.js');
worker.on('message', (result) => {
  console.log('Worker result:', result);
});
worker.postMessage([1, 2, 3, 4]);

Các Mẫu Thiết Kế Hiệu Quả

1. Hàng đợi Tác vụ (Task Queue)

Hàng đợi giúp kiểm soát mức độ đồng thời:

class TaskQueue {
  private queue: (() => Promise<void>)[] = [];
  private running = 0;

  constructor(private concurrencyLimit: number) {}

  enqueue(task: () => Promise<void>) {
    this.queue.push(task);
    this.run();
  }

  private async run() {
    if (this.running >= this.concurrencyLimit || this.queue.length === 0) return;

    this.running++;
    const task = this.queue.shift();
    if (task) await task();
    this.running--;
    this.run();
  }
}

// Usage
const queue = new TaskQueue(3);
for (let i = 0; i < 10; i++) {
  queue.enqueue(async () => {
    console.log(`Task ${i} started`);
    await new Promise((resolve) => setTimeout(resolve, 1000));
    console.log(`Task ${i} completed`);
  });
}
  1. Worker Pool cho Cân bằng Tải

Phân phối tác vụ hiệu quả bằng Worker Pool:

import { Worker, isMainThread, parentPort, workerData } from 'worker_threads';

if (isMainThread) {
  const workers = Array.from({ length: 4 }, () => new Worker(__filename));
  const tasks = [10, 20, 30, 40];

  workers.forEach((worker, index) => {
    worker.postMessage(tasks[index]);
    worker.on('message', (result) => console.log('Result:', result));
  });
} else {
  parentPort.on('message', (task) => {
    parentPort.postMessage(task * 2);
  });
}

Thách thức và Giải pháp

1. Gỡ lỗi mã không đồng bộ:

• Sử dụng async_hooks hoặc các công cụ gỡ lỗi hỗ trợ async/await.

2. Xử lý lỗi trong Promise:

• Dùng try/catch hoặc .catch() để xử lý lỗi một cách hiệu quả.

3. Điều kiện Race:

• Tránh chia sẻ trạng thái hoặc sử dụng cơ chế khóa.


Best Practices

1. Ưu tiên I/O Không đồng bộ: Tránh chặn luồng chính trong các tác vụ nặng.

2. Dùng Worker Threads cho CPU-intensive: Đẩy các tác vụ nặng sang Worker Threads hoặc Web Workers.

3. Giới hạn Xử lý Đồng thời: Sử dụng hàng đợi hoặc thư viện như p-limit để giới hạn số lượng tác vụ đồng thời.


Kết luận

Xử lý đồng thời và song song là hai khái niệm quan trọng giúp xây dựng các ứng dụng TypeScript hiệu suất cao. Trong khi xử lý đồng thời tối ưu hóa khả năng phản hồi, xử lý song song tận dụng tài nguyên đa lõi để tăng tốc độ xử lý. Bằng cách áp dụng các kỹ thuật này, nhà phát triển có thể xây dựng ứng dụng mạnh mẽ và hiệu quả.