Ở bài viết trước, chúng ta đã biết 6 pha xử lý trong một vòng lặp sự kiện, về chức năng và các kiểu hàm callback
nào được xử lý ở đâu. Ngoài ra còn có một pha đặc biệt gọi là process.nextTick
tuy không thuộc Event loop nhưng lại có mức độ ưu tiên cao nhất.
Khái niệm tưởng chừng dễ nhưng lại là gây khó dễ cho nhiều người, kể cả lập trình viên lâu năm. Vậy khi đặt process.nextTick
, setImmediate
và setTimeout
lại cùng với nhau. Theo bạn callback
trong hàm nào được thực hiện trước?
Trước tiên hãy quay lại mô hình 6 pha của vòng lặp sự kiện.
┌───────────────────────────┐
┌─>│ timers │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ pending callbacks │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ idle, prepare │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ poll │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ check │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
└──┤ close callbacks │
└───────────────────────────┘
Mỗi pha có nhiệm vụ xử lý các sự kiện riêng biệt. Chúng ta không thể đẩy ồ ạt sự kiện vào rồi mong chờ Event loop xử lý chúng một cách lần lượt, mà có những sự kiện cần được xử lý trước các sự kiện khác. Việc chia pha giúp đảm bảo điều đó.
Mỗi một chu trình lặp, Node.js đi qua từng pha và xử lý sự kiện trong đó trước khi sang pha khác. Ở đây setTimeout
được xử lý trong "timers" nằm ở trên cùng của Event loop. Trong khi setImmediate
được xử lý trong "check". Nếu vậy thì chẳng phải setTimeout
với thời gian chờ bằng 0
luôn thực hiện trước setImmediate
?
Trong một chương trình đồng bộ, tức là không gọi hàm bất đồng bộ nào cả. Nếu sử dụng setTimeout(fn, 0)
, hàm callback
sẽ được lên lịch trong pha "timers". Nếu sử dụng setImmediate()
, hàm callback sẽ được lên lịch trong pha "check". Vậy theo lý thuyết, callback
của setTimeout
luôn được thực hiện trước setImmediate
.
setTimeout(() => {
console.log('setTimeout');
}, 0);
setImmediate(() => {
console.log('setImmediate');
});
Khi chạy mã trên bạn sẽ thấy 2 chuỗi "setTimeout" và "setImmediate" xuất hiện không theo thứ tự nào cả, tức là có lúc thì "setTimeout" in ra trước, lúc thì lại in ra sau. Lý giải cho điều này là khi mới bắt đầu chương trình, Node.js có độ trễ nhất định trước khi đi vào ổn định, tức là đôi khi nó ở pha "timers", đôi khi nó ở pha "check".
Trong trường hợp chương trình có gọi hàm bất đồng bộ thứ tự thực thi sẽ khác, luôn luôn là setImmediate
trước tiên vì setImmediate
được thực thi ngay sau khi tác vụ I/O hoàn thành, vì nó nằm trong pha "check" của event loop, vốn xảy ra ngay sau pha "poll" (giai đoạn xử lý các tác vụ I/O).
setTimeout(fn, 0)
chỉ được thực thi sau khi event loop đã quay lại pha "timers", nghĩa là nó sẽ phải đợi một vòng lặp nữa để đến pha "timers".
fs.readFile("README.md", () => {
setTimeout(() => {
console.log('setTimeout');
}, 0);
setImmediate(() => {
console.log('setImmediate');
});
});
Vì vậy, chúng ta nên sử dụng setImmediate
để chạy một hàm callback
ngay sau khi tác vụ I/O được hoàn thành.
setImmediate
tỏ ra hữu ích khi được dùng để ghi nhật ký truy vấn (request logs). Ví dụ như trong một máy chủ HTTP dưới đây:
res.writeHead(200, { "Content-Type": "text/plain" });
res.end("Hello World\n");
// ghi log vào file (công việc ít quan trọng hơn)
setImmediate(() => {
fs.appendFile("server.log", `Request: ${req.url}\n`, (err) => {
if (err) throw err;
console.log("Logged request to server.log");
});
});
Nhiều người nghĩ rằng không cần dùng đến setImmediate
ở đây vì fs.appendFile
là một hàm bất đồng bộ và nó không gây ảnh hưởng gì đến tốc độ phản hồi của phiên hiện tại. Do đó có thể loại bỏ setImmediate
để tránh mã thừa.
Điều đó chưa chắc đã đúng. Thực tế máy chủ luôn luôn nhận được rất nhiều truy vấn đến cùng một lúc. Việc sử dụng setImmediate
giúp đẩy phần công việc ghi nhật ký vào pha "check", nhường thời gian cho pha "poll" - vốn được dùng để xử lý callback
của các chức năng chính, tăng hiệu suất máy chủ. Luôn luôn nhớ rằng đẩy các công việc bớt quan trọng hơn xuống pha "check", nhường thời gian xử lý cho "poll".
Ngoài ra setImmediate
còn được sử dụng để giảm tải luồng chính. Để biết thêm thông tin chi tiết, bạn đọc xem bài viết Hai kỹ thuật nhằm ngăn vòng lặp sự kiện (Event Loop) bị chặn khi xử lý tác vụ nặng (CPU-intensive task). Trong bài viết đề cập đến phương pháp "Partitioning" dựa trên setImmediate
để ngăn chặn vòng lặp sự kiện khi muốn xử lý một lượng lớn dữ liệu.
Nhìn lại 6 pha của Event loop bạn không thấy bất kỳ pha nào xử lý callback
của process.nextTick
. Đó là do process.nextTick
là một pha đặc biệt, callback
của nó luôn luôn được xử lý trên đầu mỗi pha của vòng lặp sự kiện.
Ví dụ, Event loop xử lý hết callback
của "timers", chuẩn bị chuyển sang "pending callbacks" thì hẵng khoan đã, nó xử lý callback trong process.nextTick
, sau khi xong hết thì mới chuyển sang "pending callbacks"... và cứ như thế ở các pha tiếp theo. Như vậy tức là process.nextTick
có độ ưu tiên rất cao trong Event loop.
Nên ứng dụng process.nextTick
như thế nào? Câu trả lời rất rõ ràng: Bất cứ khi nào muốn thực hiện công việc có mức ưu tiên cao nhất, trước khi các pha được xử lý để đảm bảo công việc được thực hiện ngay lập tức.
Ví dụ process.nextTick
cũng thường được áp dụng trong kỹ thuật "Partitioning" tương tự như setImmediate
, chỉ có điều mỗi vòng lặp chỉ có 1 lần xử lý setImmediate
, trong khi với process.nextTick
thì con số lên đến 6 lần.
Vì lẽ đó trong thư viện bcrypt được dùng để tính toán chuỗi mã hoá - một tác vụ rất nặng về tính toán và có nguy cơ chặn Event loop đã áp dụng triệt để process.nextTick
ngăn ngừa hành vi này.
Qua bài viết này, chúng ta đã đi sâu vào cách hoạt động của process.nextTick
, setImmediate
và setTimeout
trong Node.js, từ đó hiểu rõ vai trò của chúng trong Event loop. process.nextTick
nổi bật với mức ưu tiên cao nhất, được thực thi ngay trước khi chuyển sang các pha khác của Event loop, là lựa chọn lý tưởng cho những công việc cần thực hiện ngay lập tức. Trong khi đó, setImmediate
được ưu tiên sử dụng để xử lý các tác vụ ít quan trọng hơn sau khi hoàn thành I/O, giúp giảm tải cho vòng lặp sự kiện và tối ưu hiệu suất hệ thống. Còn setTimeout
thường được dùng để lên lịch thực thi ở các vòng lặp sau, nhưng lại dễ bị ảnh hưởng bởi độ trễ ban đầu của Node.js.
Hiểu rõ sự khác biệt này không chỉ giúp lập trình viên sử dụng các hàm callback một cách hiệu quả hơn, mà còn tối ưu hóa hiệu suất ứng dụng, đặc biệt trong các hệ thống đa nhiệm và yêu cầu xử lý đồng thời cao. Tùy thuộc vào kịch bản, việc chọn đúng công cụ giữa process.nextTick
, setImmediate
và setTimeout
có thể tạo ra sự khác biệt lớn trong cách ứng dụng của bạn hoạt động và phản hồi. Hãy sử dụng chúng một cách khôn ngoan để đảm bảo rằng các tác vụ được xử lý theo đúng thứ tự ưu tiên và phù hợp với yêu cầu của hệ thống.
Tôi & khao khát "chơi chữ"
Bạn đã thử viết? Và rồi thất bại hoặc chưa ưng ý? Tại 2coffee.dev chúng tôi đã có quãng thời gian chật vật với công việc viết. Đừng nản chí, vì giờ đây chúng tôi đã có cách giúp bạn. Hãy bấm vào để trở thành hội viên ngay!
Đăng ký nhận thông báo bài viết mới
Bình luận (1)