Child process trong Node.js là gì? - Khi nào cần sử dụng fork và spawn?

Child process trong Node.js là gì? - Khi nào cần sử dụng fork và spawn?

Những mẩu tin ngắn hàng ngày dành cho bạn
  • Mọi người đã nghe nói đến Jujutsu - jj - một dạng quản lý phiên bản cho mã nguồn (version control system) chưa? Có vẻ như nó đang nhận được nhiều sự quan tâm.

    Chờ xíu! Chẳng phải git đã quá tốt rồi sao? Thế thì chế ra thằng jj để làm gì nữa? Cũng hơi khó trả lời nhỉ? Mỗi công cụ sinh ra chắc chắn phải cải thiện hoặc khắc phục được nhược điểm của cái trước. Cho nên jj ắt hẳn phải làm được điều gì đó mà git chưa làm được nên mới nổi lên như vậy.

    Thật ra mình đã nghe nói đến jj từ vài tháng trước rồi, nhưng vào đọc thì toàn kiến thức cao siêu. Hoặc là đang mang nặng cái lối suy nghĩ của git vào trong đầu rồi nên chưa lĩnh hội ra được điều gì cả.

    Mình hay có kiểu cái gì đọc lần 1 mà không hiểu thì đọc tiếp lần 2, lần 2 không hiểu thì đọc tiếp lần 3... đến lần thứ n mà vẫn không hiểu thì bỏ. Cơ mà không phải là từ bỏ mà một thời gian sau đó quay lại đọc tiếp. Đến một lúc nào đó khả năng mình sẽ hiểu ra một ít vấn đề, thế mới tài 😆.

    Thì cái jj này có vẻ như nó đang mở ra được tính linh hoạt trong việc "cam kết" mã. Tưởng tượng bạn đang làm việc trên một dự án, đang ở nhánh này, muốn sang nhánh khác để sửa, nhưng mà lại đang viết dở ở nhánh này, thế là phải stash, rồi checkout, rồi commit, rồi merge hoặc rebase lại vào nhánh cũ... nhìn chung quá trình làm việc với git nghiêm ngặt đến mức cứng nhắc, cần nhiều thao tác để giải quyết một vấn đề, chưa kể cái cây commit (commit-tree) nữa thì ôi thôi, khỏi xem cho đỡ nhức mắt. Thế nên ông jj này đang làm cách nào đó để bạn khỏi cần phải quan tâm đến các nhánh luôn, sửa trực tiếp vào commit. Nghe ảo nhỉ 😂.

    Đấy mới lĩnh hội được đến đấy, hy vọng sau n lần đọc lại nữa mình sẽ viết được một bài chi tiết hơn về công cụ này.

    » Xem thêm
  • Gòi gòi tới công chiện gòi 🤤🤤🤤

    » Xem thêm
  • Không biết blog được dẫn nguồn từ trang cà phê nào hay sao mà vài ba hôm trở lại đây thấy nhiều người tìm kiếm cà phê thế không biết 🤔.

    Tìm cả cách pha với tìm cả loại hạt, khổ nỗi họ tìm lại không ra bài viết nào vì mình chưa có viết đến mấy trường hợp đó. Phải chăng là ý trời 😀🙏

    » Xem thêm

Vấn đề

Có một lời khuyên mà chắc hẳn ai làm việc với Node.js đều phải thuộc lòng đó là "đừng bao giờ chặn vòng lặp sự kiện". Chặn ở đây có nghĩa là không để cho Event Loop được thực thi chức năng vốn có của nó. Node.js chỉ có một luồng để xử lý mã JavaScript, nếu một công việc chiếm tương đối thời gian để xử lý thì nó sẽ gây ra một cuộc tắc nghẽn nghiêm trọng trong luồng chính. Hay nói cách khác, tất cả cuộc gọi API có thể sẽ không bao giờ được phản hồi cho đến khi công việc đó xong.

Biết được vấn đề, tất nhiên Node.js phải cung cấp cho chúng ta một số cách để giải quyết. Thay vì gọi những hàm đồng bộ thì hãy chuyển qua gọi hàm bất đồng bộ, ví dụ như cùng là đọc file nhưng readFile sẽ được khuyên dùng hơn readFileSync bởi vì readFile là hàm bất đồng bộ. Ngoài ra, nếu công việc đòi hỏi khả năng tính toán cao của CPU như xử lý hình ảnh, video... thì có một giải pháp khác là sử dụng module child_process được tích hợp trong Node.

Có thể nói child_process là giải pháp sớm nhất mà Node.js đưa ra, sau này chúng ta có thêm module worker_threads cũng có khả năng giải quyết được vấn đề chặn vòng lặp sự kiện. Tôi đã có một bài về Worker threads là gì? Bạn đã biết khi nào thì sử dụng Worker threads trong node.js chưa?, bạn đọc có thể tham khảo thêm về khái niệm cũng như cách dùng nó. Nhưng trong phạm vi bài viết này, chúng ta hãy tạm quên đi Worker threads và tập trung vào xem Child process là gì và nó được sử dụng như thế nào nhé.

Child process là gì?

Child process là một module của Node.js cho phép tạo ra các tiến trình con (process) độc lập để thực hiện các tác vụ cụ thể. Nó cho phép Node.js chạy nhiều tác vụ đồng thời và tận dụng tối đa sức mạnh của máy chủ. Khi tạo ra một child process, nó sẽ chạy độc lập với parent process (tiến trình cha) và có thể giao tiếp với cha qua luồng (stream), các sự kiện (event)... Các child process được tạo ra có tài nguyên độc lập, giúp giảm thiểu tác động đến các tiến trình khác khi xử lý tác vụ nặng hoặc chẳng may bị lỗi.

Cho dễ hình dung, một ứng dụng Node.js khi khởi động thì nó là một process với một bộ V8 Engine được tạo ra. Để ngăn vòng lặp sự kiện bị chặn, cách tốt nhất là tạo ra một tiến trình khác để xử lý. Khi đó, nó có thể chạy độc lập với tiến trình cha, xử lý rồi trả lại kết quả cho tiến trình cha thông qua một kênh giao tiếp như đã kể đến ở bên trên.

Tùy thuộc vào cách child process được tạo ra mà nó có cách thực hiện nhiệm vụ khác nhau. Có hai cách phổ biến để tạo ra child process là spawnfork. Trong khi fork cố gắng tạo ra một "bản sao" của process cha, có nghĩa là "clone" ra một V8 Engine để xử lý tác vụ thì spawn lại chỉ đơn giản là tạo ra một process thực hiện câu lệnh nào đó. Chi tiết hơn, chúng ta hãy đi qua từng phương thức xem chúng thực chất là như thế nào.

spawn

spawn là một phương thức để tạo ra một child process mới. Khi sử dụng spawn, ta có thể truyền cho child process các tham số, tùy chọn và đối số cần thiết để thực thi lệnh hoặc file thực thi.

child_process.spawn(command[, args][, options])

Khi child process được tạo ra bằng spawn, nó có thể hoạt động độc lập với process cha, và có thể trao đổi dữ liệu với process cha thông qua pipe hoặc stream. Ta cũng có thể quản lý child process bằng cách theo dõi các sự kiện để biết khi nó hoàn thành hoặc gặp lỗi.

Ví dụ về cách sử dụng spawn hết sức đơn giản:

const { spawn } = require('child_process');
const ls = spawn('ls', ['-lh', '/usr']);

ls.stdout.on('data', (data) => {
  console.log(`stdout: ${data}`);
});

ls.stderr.on('data', (data) => {
  console.error(`stderr: ${data}`);
});

ls.on('close', (code) => {
  console.log(`child process exited with code ${code}`);
});

Ở dòng số 2 chúng ta đang tạo ra một child process và nó thực hiện câu lệnh ls với các tùy chọn '-lh' và '/usr'. Hay nói cách khác, đó là một command:

$ ls -lh /usr

Sau đó, sử dụng on để lắng nghe sự kiện từ child pocess để nhận dữ liệu ở process cha. Trong ví dụ trên, on đang "lắng nghe" trên 3 sự kiện của child process là thành công, thất bại và đóng tiến trình con.

Nếu để ý, có thể thấy trong spawn có thể chạy một lệnh node:

spawn('node', ['index.js']);

Bạn có thể chạy một file .js bằng cách trên trong tiến trình mới, hoặc nhanh hơn là sử dụng fork để đơn giản hóa khả năng sử dụng như trong phần dưới đây.

fork

fork cũng là một phương thức để tạo ra một child process mới, nó là một trường hợp đặc biệt của spawn, hay nói cách khác fork chỉ là một hàm dựa trên spawn. Child process này sẽ chạy một phiên bản độc lập của mã JavaScript được chỉ định. Mã này có thể được đặt trong một file hoặc một function được truyền dưới dạng tham số cho hàm fork.

child_process.fork(modulePath[, args][, options])

Hàm fork sẽ tạo ra một child process mới, được "sao chép" từ process cha (bao gồm những thứ như tạo ra hẳn một bộ V8 engine mới - điều này làm cho fork trở nên tốn kém về mặt tài nguyên), nhưng với một môi trường độc lập và một ID process khác biệt. Child process này có thể thực hiện các nhiệm vụ độc lập với process cha, có thể giao tiếp với process cha thông qua một kênh IPC (Inter-Process Communication) được cung cấp bởi Node.js.

Với fork, ta có thể sử dụng các child process để chia sẻ tải công việc, xử lý các tác vụ nặng, chạy các đoạn mã không đồng bộ mà không ảnh hưởng đến performance của process cha.

Ví dụ, bạn có một file fibonacci.js đơn giản như sau:

function fibonacci(n) {
  if (n < 2) {
    return n;
  } else {
    return fibonacci(n - 1) + fibonacci(n - 2);
  }
}

process.on('message', (msg) => {
  const result = fibonacci(msg);
  process.send(result);
});

Sau đó, tạo ra một child process để xử lý việc gọi hàm fibonacci() trong một tiến trình riêng biệt.

const { fork } = require('child_process');

const child = fork('fibonacci.js');

child.on('message', (result) => {
  console.log(`Fibonacci: ${result}`);
});

child.send(10);
});

Khi nào nên sử dụng child process cũng như fork hoặc spawn?

Trước tiên, phải nói rằng lựa chọn sử dụng child process còn phụ thuộc vào bài toán đang cần giải quyết. Vì chi phí để tạo ra một process là khá tốn kém cho nên không phải cứ tạo ra càng nhiều child process thì ứng dụng của bạn sẽ xử lý càng nhanh. Ngược lại, nó có thể nhanh chóng làm cạn kiệt tài nguyên máy chủ cũng như chi phí liên lạc giữa các tiến trình với nhau.

Node.js xử lý rất tốt I/O không đồng bộ, nếu ứng dụng xử lý nhiều về I/O có thể cần phải quan tâm đến cấu hình sao cho Worker Pools trong libuv được tối ưu nhất chứ không phải là tạo ra nhiều child process để xử lý I/O không đồng bộ. Bạn đọc có thể tham khảo thêm bài viết Phân biệt tác vụ I/O và tác vụ chuyên sâu CPU để biết cách phân biệt tác vụ I/O với Tác vụ chuyên sâu CPU.

Trong trường hợp ứng dụng cần sự tính toán của CPU nhiều hơn thì child process thực sự phù hợp. Lúc này cần phải vận dụng kinh nghiệm sử dụng hai cách tạo ra tiến trình con đã nêu ra ở trên để tối ưu chi phí tài nguyên.

Ví dụ bạn có cài một phần mềm, lệnh, bash script... trong máy chủ và muốn gọi chúng từ Node.js thì hãy sử dụng spawn. Nó chỉ đơn giản tạo ra một tiến trình để thực hiện câu lệnh trong spawn rồi trả về kết quả, vừa nhanh vừa tiết kiệm.

fork thì lại phù hợp trong trường hợp công việc "High CPU" nằm trong một file hoặc một hàm JavaScript. fork tạo ra một bản sao V8 Engine và có toàn quyền truy cập vào những module (node_modules) có trong ứng dụng của bạn. Hơn nữa, vì là tiến trình độc lập nên chẳng may tiến trình bị lỗi thì không gây ảnh hưởng nghiêm trọng đến ứng dụng của bạn cả.

Nói công việc nặng thì có vẻ trừu tượng, hãy lấy một ví dụ cụ thể là xử lý hình ảnh - đó là một trong những công việc đòi hỏi khả năng tính toán của CPU như resize, filter, cân bằng sáng tối... Bạn có thể tìm được rất nhiều ứng dụng dạng command line (cli) để cài vào máy, hoặc tìm kiếm các thư viện xử lý hình ảnh của Node.js. Nếu dùng cli tiện hơn, hãy dùng spawn, còn sử dụng thư viện, hãy dùng fork...

Tổng kết

Child process là một module của Node.js cho phép tạo ra các quy trình con độc lập để thực hiện các tác vụ cụ thể, nhằm ngăn việc chặn vòng lặp sự kiện. Có nhiều cách để tạo ra một child process thông qua module child_process, trong số đó là hai phương thức spawnfork. spawn thì được dùng để chạy một lệnh cụ thể trong khi fork tạo ra một bản sao của V8 Engine để chạy một đoạn mã JavaScript. Tùy vào bài toán mà việc lựa chọn sử dụng child process sao cho hợp lý, tránh lãng phí tài nguyên cũng như tăng hiệu suất cho ứng dụng của bạn.

Tài liệu tham khảo:

Cao cấp
Hello

5 bài học sâu sắc

Mỗi sản phẩm đi kèm với những câu chuyện. Thành công của người khác là nguồn cảm hứng cho nhiều người theo sau. 5 bài học rút ra được đã thay đổi con người tôi mãi mãi. Còn bạn? Hãy bấm vào ngay!

Mỗi sản phẩm đi kèm với những câu chuyện. Thành công của người khác là nguồn cảm hứng cho nhiều người theo sau. 5 bài học rút ra được đã thay đổi con người tôi mãi mãi. Còn bạn? Hãy bấm vào ngay!

Xem tất cả

Đăng ký nhận thông báo bài viết mới

hoặc
* Bản tin tổng hợp được gửi mỗi 1-2 tuần, huỷ bất cứ lúc nào.

Bình luận (1)

Nội dung bình luận...
Avatar
Jess Vanes1 năm trước
quưkdnqmxncks skdnc akaofmxnak
Trả lời
Bấm hoặc cuộn mạnh để sang bài mới