0

Làm chủ tính đồng thời và tính song song trong TypeScript

Các ứng dụng hiện đại đòi hỏi hiệu suất cao và khả năng phản hồi nhanh, yêu cầu các nhà phát triển phải nắm vững xử lý đồng thời và song song. TypeScript, với tư cách là một siêu tập của JavaScript, cung cấp các công cụ và mẫu mạnh mẽ để quản lý những sự phức tạp này.

Xử lý Đồng thời vs. Song song: Sự khác biệt chính

Trước khi đi vào code, điều quan trọng là phải hiểu các thuật ngữ này:

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

  • Định nghĩa: Khả năng của một hệ thống xử lý nhiều tác vụ bằng cách xen kẽ việc thực thi của chúng (không nhất thiết phải cùng một lúc).
  • Ví dụ: Chuyển đổi giữa việc xử lý một truy vấn cơ sở dữ liệu và xử lý việc tải lên tệp trong một vòng lặp sự kiệ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 tận dụng bộ xử lý đa lõi.
  • Ví dụ: Thực hiện các phép tính toán học phức tạp trên các lõi khác nhau đồng thời.

VD: Hãy tưởng tượng một nhà hàng:

  • Xử lý Đồng thời: Một đầu bếp duy nhất đa nhiệm giữa nhiều món ăn.
  • Xử lý Song song: Nhiều đầu bếp làm việc trên các món ăn riêng biệt cùng một lúc.

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

JavaScript, và mở rộng là TypeScript, chạy trên một vòng lặp sự kiện đơn luồng, điều này có thể khiến xử lý đồng thời nghe có vẻ bất khả thi. Tuy nhiên, xử lý đồng thời đạt được thông qua các mô hình lập trình không đồng bộ như callbacks, promises, và async/await.

1. Sử dụng Promises cho Xử lý Đồng thời

Promises là một trong những cách đơn giản nhất để đạt được xử lý đồng thời trong TypeScript.

const fetchData = (url: string) => {
  return new Promise<string>((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: Promise.all cho phép cả hai thao tác tìm nạp chạy đồng thời, tiết kiệm thời gian.

2. Xử lý Đồng thời với Async/Await

async/await đơn giản hóa việc xâu chuỗi promise trong khi vẫn duy trì tính chất không đồng bộ.

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();

Xử lý Song song trong TypeScript

Mặc dù JavaScript không hỗ trợ đa luồng một cách nguyên bản, Web Workers và Node.js Worker Threads cho phép xử lý song song. Các tính năng này tận dụng các luồng riêng biệt để xử lý các tác vụ tốn kém về mặt tính toán.

1. Web Workers cho Xử lý Song song

Trong môi trường trình duyệt, Web Workers thực thi các script trong một 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]);

2. Node.js Worker Threads

Đối với các ứng dụng phía máy chủ, Node.js cung cấp worker_threads.

// 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 cho Xử lý Đồng thời và Song song hiệu quả

1. Hàng đợi Tác vụ để Quản lý Xử lý Đồng thời

Khi xử lý nhiều tác vụ, hàng đợi tác vụ đảm bảo việc thực thi được kiểm soát.

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`);
  });
}

2. Cân bằng Tải với Workers Pool

Workers Pool phân phối hiệu quả các tác vụ trên nhiều workers.

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 các công cụ như async_hooks trong Node.js để theo dõi các hoạt động không đồng bộ.
  • Sử dụng IDE hỗ trợ gỡ lỗi mã async/await.

2. Xử lý lỗi

Bọc promises trong các khối try/catch hoặc sử dụng .catch() với Promise.all.

3. Điều kiện Race

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

Best practice cho Xử lý Đồng thời và Song song

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

  2. Sử dụng Worker Threads cho các Tác vụ Chuyên sâu về CPU: Dỡ bỏ các phép tính 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 tác vụ hoặc các thư viện như p-limit để kiểm soát mức độ xử lý đồng thời.

  4. Tận dụng các Thư viện: Sử dụng các thư viện như Bull cho hàng đợi tác vụ hoặc Workerpool để quản lý worker thread.

Kết luận

Xử lý đồng thời và song song rất quan trọng để xây dựng các ứng dụng TypeScript hiệu suất cao, có khả năng mở rộng. Trong khi xử lý đồng thời cải thiện khả năng phản hồi bằng cách xen kẽ các tác vụ, xử lý song song cho phép thực thi đồng thời trên các hệ thống đa lõi. Bằng cách nắm vững các khái niệm này, các nhà phát triển có thể giải quyết các thách thức trong các ứng dụng hiện đại và mang lại trải nghiệm người dùng liền mạch.


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí