Hướng dẫn Multithread với Node.js

Đa luồng – Multithread với Node.js???

Node.js đã từng bị chỉ trích rất nhiều vì thiết kế của nó.
So sánh với các ngôn ngữ khác như Java, C hay Python, có vẻ khá kỳ lạ khi Node.js không hỗ trợ truy cập trức tiếp đến threads. Vậy làm sao chúng ta có thể thực hiện các tác vụ đồng thời?

Vâng, trước Node.js 11 chúng ta có thể sử dụng cluster module. Nhưng nếu server chỉ có duy nhất một nhân thì phải làm sao?

Thật may là ở phiên bản Node.js 11 chúng ta có vị cứu tinh worker_thread module. Module này cho phép chúng ta sinh ra nhiều threads chạy trên đơn lõi. Chúng ta cũng có thể sử dụng module này với flag –experimental-worker ở Node.js 10 nhưng mình khuyến cáo là không nên.

Một ví dụ đơn giản dễ hình dung

Giả sử chúng ta cần tạo ra một file chưa 1 triệu users với họ tên đầy đủ.
Mình có tìm được một Github repo cũng cấp cho chúng ta một mảng danh sách các họ tên để phục vụ cho ví dụ này: https://github.com/dominictarr/random-name

Đầu tiên hãy tạo một project mới với cấu trúc như sau:

Bắt đầu với main.js:

const fs = require("fs-extra");
const {
    getRandomIndex
} = require("./utils")
const firstName = require("./data/first_name.json");
const middleName = require("./data/middle_name.json");
const lastName = require("./data/last_name.json");

const limit = 1000000;
const outputFile = `${__dirname}/output/data.txt`;

(async () => {
    for (let i = 0; i < limit; i++) {
        const data = [firstName, middleName, lastName]
            .map(getRandomIndex)
            .concat("\n")
            .join(" ");
        await fs.appendFile(outputFile, data);
    }
})();

Như các bạn có thể thấy, chúng ta sử dụng package fs-extra. Nó xử lý tương tự như module fs, nhưng sẽ trả về promise cho mỗi function.

Nó giải quyết một vấn đề lớn đối với hệ thống, đó là dung lượng bộ nhớ. Sự thật là nếu chúng ta cố gẳng mở quá nhiều file với Node.js, nó sẽ sinh ra lỗi và kill process chính. Bởi vì nó không thể xử lý quá nhiều file mở cùng một lúc (tràn bộ nhớ).

Trong vòng lặpfor của chúng ta, await sẽ dừng vòng lặp cho đến khi tác vụ kết thúc. Bằng cách này chúng ta sẽ chỉ xử lý một file cho mỗi lần lặp.

function getRandomIndex(array) {
    return array[Math.floor(Math.random() * array.length)];
}

module.exports = {
    getRandomIndex
}

Ở đây chúng ta chỉ lấy những giá trị ngẫu nhiên từ bất kỳ mảng nào. Sử dụng để trộn ngẫu nhiên họ tên.

Chạy đoạn code trên laptop cá nhân (2016 MacBook Pro, 2,7 GHz Intel Core i7, 16GB RAM) nó mất khoảng 3 phút và 32 giây để hoàn thành.

Thử sử dụng Node.js worker threads để xem hiệu suất có khác biệt không nhé!

Triển khai multithread với Node.js

Để triển khai multithread cho chương trình này, chúng ta cần thay đổi một số chỗ trong code. Bắt đầu với main.js file:

const {
    Worker
} = requirer("worker_threads");
const logUpdate = require("log-update");

const limit = 1000000;
const threads = 10;
const namesPerThread = limit / threads;
const outputFile = `${__dirname}/output/data.txt`;
let names = [...Array(threads)].fill(0);

for (let i = 0; i < threads; i++) {
    const port = new Worker(require.resolve("./worker.js"), {
        workerData: {
            namesPerThread,
            outputFile
        }

    });
    port.on("message", (data) => handleMessage(data, i));
    port.on("error", (e) => console.log(e));
    port.on("exit", (code) => console.log(`Exit code: ${code}`));
}

function handleMessage(_, index) {
    names[index]++;
    logUpdate(names.map((status, i) => `Thread ${i}: ${status}`).join("\n"));
}
  • Đầu tiên chúng ta cần import Worker class từ worker_threads module. Việc này cho phép chúng ta sinh ra các worker bất cứ lúc nào.
  • Sau đó chúng ta cần thiết lập số lượng các threads cần được sinh ra. Trong trường hợp này mình quyết định sinh ra 10 threads.
  • Chúng ta cần tính toán có bao nhiêu tên cần được tạo ra trên mỗi thread. Chỉ cần chia tổng số tên hiện tại cho tổng số threads.
  • Với mỗi thread, chúng ta cần sinh ra một Worker mới. Code sẽ được đặt trong file worker.js.
  • Chúng ta gửi một payload đến Worker mới để cho biết số tên cần được tạo ra và nơi để lưu chúng.

Hãy xem cách mà worker.js hoạt động:

const {
    getRandomIndex
} = require("./utils");
const {
    parentPort,
    workerData
} = require("worker_threads")
const fs = require("fs-extra");

const firstName = require("./data/first_name.json");
const middleName = reguire("./data/middle_name.json");
const lastName = require("./data/last_name.json");
const {
    namesPerThread,
    outputFile
} = workerData;

~function async() {
    for (let i = 0; i < namesPerThread; i++) {
        const data = [firstName, middleName, lastName]
            .map(getRandomIndex)
            .concat("\n")
            .join(" ");
        await fs.appendFile(outputFile, data);
        parentPort.postMessage(data);
    }
}()

Về cơ bản nó giống hệt code của main.js. Tuy nhiên mỗi khi chúng ta lưu một tên mới, chúng ta gửi trả về thread chính để theo dõi những gì diễn ra bên trong các threads phụ.

Vậy kết quả như thế nào? Chúng ta thực hiện cùng một công việc nhưng chỉ mất 1 phút và 24 giây! Nhanh hơn 37% so với khi chỉ sử dụng một thread!

Những ứng dụng khác sử dụng multithread với Node.js

Worker Threads là một giải pháp tuyệt vời khi bạn cần thực hiện một tác vụ chuyên sâu với CPU. Chúng làm cho các hoạt động liên quan đến hệ thống tập tin nhanh hơn và giúp ích rất nhiều khi bạn cần thực hiện bất kỳ loại hoạt động đồng thời nào. Điều tuyệt vời nhất, như mình đã nói trước đây, chúng cũng hoạt động trên các máy đơn lõi, vì vậy hứa hẹn một hiệu suất tốt hơn trên bất kỳ máy chủ nào.

Mình thường sử dụng Worker Threads trong các tác vụ upload hàng loạt, nơi mình phải kiểm tra hàng triệu người dùng và lưu trữ dữ liệu của họ vào cơ sở dữ liệu. Áp dụng cách tiếp cận đa luồng, thao tác đã nhanh hơn khoảng 10 lần so với thao tác đơn luồng trên cùng một tác vụ.

Mình cũng sử dụng Worker Threads cho việc xử lý ảnh. Công việc cần làm là xây dựng các thumbnails (với kích thước khác nhau) từ một hình ảnh và sử dụng multithread giúp tiết kiệm rất nhiều thời gian cũng như tài nguyên của server.

Kết luận

Từ những ứng dụng nêu trên, các bạn có thể thấy được module Worker Thread có thể giúp chúng ta rất nhiều trong việc nâng cao hiệu suất, tiết kiệm tài nguyên và giúp cho công việc trở nên thật sự hiệu quả

Hy vọng sau bài hướng dẫn này, các bạn có thể ứng dụng được multithread vào trong dự án Node.js của mình!

>
Secured By miniOrange