Tìm hiểu về vòng lặp sự kiện (Event Loop) trong Node.js

Tìm hiểu về vòng lặp sự kiện (Event Loop) trong Node.js

Tin ngắn hàng ngày dành cho bạn
  • Manus đã chính thức mở cửa cho tất cả người dùng rồi đấy mọi người. Cho những ai chưa biết thì đây là một công cụ viết báo cáo (làm mưa làm gió) giống như Deep Research của OpenAI á. Mỗi ngày được miễn phí 300 Credits để nghiên cứu. Mỗi lượt nghiên cứu tiêu tốn tuỳ thuộc vào độ phức tạp của yêu cầu. À với cả họ đang có chương trình tặng miễn phí Credits hay sao á. Như mình thì vào thấy được hẳn 2000.

    Mình dùng thử, so sánh với cùng một lệnh giống như đợt trước dùng bên Deep Research thì nội dung khác biệt nhau hoàn toàn. Manus báo cáo như kiểu viết văn hơn so với OpenAI là các gạch đầu dòng và bảng biểu.

    À lúc đăng ký xong có bắt nhập số điện thoại để xác minh, nếu lỗi thì các bạn đợi qua ngày thử lại xem có được không nhé.

    » Xem thêm
  • Mọi người chắc nghe nhiều về xu hướng tìm kiếm thông tin bằng AI chứ không cần công cụ tìm kiếm như Google nữa rồi đúng không? Không đâu xa ánh xạ vào bản thân thì thấy đúng thật, thi thoảng mới tìm kiếm thôi chứ còn đâu toàn hỏi tụi AI.

    Ngay từ đầu viết blog, thứ mà mình hướng đến là chia sẻ kinh nghiệm chứ không phải là những bài mang nặng tính kỹ thuật, máy móc, hướng dẫn từ đầu... Vì thời điểm đó đã có quá nhiều người làm nội dung này rồi và họ làm rất tốt, tại sao mình phải cố phát minh lại bánh xe? Một điều nữa là tin tưởng độc giả của mình có khả năng tìm hiểu vấn đề. Nếu bạn đọc đủ nhiều các bài viết trên blog thì thấy mình luôn cố gắng chèn thêm các liên kết tham khảo ngoài bài viết, nêu ra vấn đề mở và rất ít khi kết luận chắc chắn một điều gì đó.

    Mình đã cố gắng rèn luyện kỹ năng viết, kỹ năng trình bày và cả cách tương tác với độc giả để mang lại giá trị cho họ. Nhiều lúc ngồi lật lại các con số thống kê thấy lượng đọc bài viết tăng lên lại cảm thấy vui. Nhưng khi nguồn truy cập đến từ Google thì lại thấy buồn, vì điều đó chứng tỏ họ biết đến mình chỉ khi đang cố đi tìm giải pháp, có thể họ chỉ đọc chớp nhoáng, may ra tìm được cách giải quyết và thế là đóng cửa sổ trình duyệt rồi đi như một cơn gió.

    Chừng vài tháng đổ lại đây, một điều khiến mình rất vui đó là lượng người truy cập thẳng vào trang chủ mà không thông qua công cụ tìm kiếm đang tăng dần lên, có nhiều hôm lượng truy cập tự nhiên còn cao hơn cả đến từ Google. Điều đó chứng tỏ độc giả đã có thói quen quay lại trang của mình nhiều hơn và họ tìm thấy được giá trị từ blog mang lại. Vui mừng khôn xiết 🤩

    Bên cạnh đó thì lượng truy cập vào chuyên mục Threads - tức là mục mình đang viết bài này đang cao hơn bao giờ hết. Điều đó chứng tỏ xu hướng đi theo tin nhanh là đúng đắn. Mình có thể ngồi cả ngày để viết tin ngắn cho bạn đọc vì nó rất nhanh mà tiện, không tốn công đi tìm tài liệu để viết, không tốn cả thời gian viết nữa, còn mình thì có rất nhiều thứ để chia sẻ 😅. Nhưng không vì thế mà bỏ bê các bài viết dài, vì dài thì có nhiều thông tin để chia sẻ hơn.

    Vài lời tâm sự thế thôi chứ hơn một tháng nay mình chưa viết bài viết mới nào vì công việc bận quá. Xong lâu dần cứ trì hoãn lại thành lười. À với cả tháng 5 này rất thích hợp để đọc các cuốn sách về cách mạng á. Có hôm đọc đến 2 giờ sáng mới đi ngủ 🥱

    » Xem thêm
  • Mình mới nhìn thấy một trang web khá thú vị nói về các cột mốc đáng nhớ trong lịch sử phát triển Internet toàn cầu: Internet Artifacts

    Chỉ từ 1977 - khi Internet còn nằm trong hộp thí nghiệm thì nhìn xem - giờ đây Internet đã khiến mọi thứ phát triển đến mức nào 🫣

    » Xem thêm

Event Loop là gì?

Event Loop là thứ cho phép node.js thực hiện các tác vụ I/O không đồng bộ, mặc dù trên thực tế Javascript là đơn luồng bằng cách giảm tải các hoạt động cho nhân hệ điều hành bất cứ khi nào có thể.

Vì hầu hết các hạt nhân hiện đại là đa luồng, chúng có thể xử lý nhiều tác vụ thực thi ở chế độ nền (background). Khi một trong những task này hoàn thành, hạt nhân thông báo cho node.js để hàm callback đính kèm có thể được thêm vào hàng đợi poll và chờ được thực thi.

Event Loop hoạt động như thế nào?

Khi node.js khởi động, nó khởi tạo Event Loop, xử lý tập lệnh đầu vào được cung cấp (hoặc REPL) có thể bao gồm việc thực hiện các hàm không đồng bộ, schedule timers hoặc process.nextTick(), sau đó bắt đầu xử lý Event Loop.

Sơ đồ sau đây cho thấy một cái nhìn tổng quan đơn giản về thứ tự hoạt động của Event Loop.

Các pha của event loop

Lưu ý: mỗi khối được coi là một "phase" (pha) của vòng lặp sự kiện.

Mỗi pha có một hàng đợi FIFO chứa các hàm callbacks. Mỗi giai đoạn đều có một nhiệm vụ riêng, nhưng nói chung khi Event Loop bước vào một giai đoạn nhất định, nó sẽ xử lý bất kỳ dữ liệu nào cho giai đoạn đó, sau đó thực hiện các hàm callbacks trong hàng đợi của pha đó cho đến khi hết hoặc đạt đến giới hạn thực thi. Tiếp đến Event Loop sẽ chuyển sang các giai đoạn tiếp theo.

Vì mỗi pha có thể có một số lượng lớn các hàm callbacks chờ được xử lý thế nên một số callback của các hàm timers (bộ đếm thời gian) có thể sẽ có thời gian chờ thực hiện lâu hơn là so với ngưỡng ban đầu đặt ra, ngưỡng thời gian ban đầu chỉ đảm bảo thời gian chờ ngắn nhất chứ không phải là thời gian chờ chính xác.

Ví dụ

setTimeout(() => console.log('hello world'), 1000);

Thì 1000ms là thời gian chờ ngắn nhất, chứ không phải là sau đúng 1000ms lệnh console.log sẽ được thực hiện.

Tổng quan về các pha (phase) của Event Loop

  • Timers: thực thi các hàm callbacks đã được lên lịch với setTimeout()setInterval().
  • Pending callbacks: thực hiện các I/O callbacks được hoãn lại cho lần lặp tiếp theo.
  • Idle, prepare: dùng cho việc xử lý nội bộ của node.js.
  • Poll: truy xuất các sự kiện I/O mới, thực hiện các hàm callbacks liên quan đến I/O (hầu như tất cả ngoại trừ close callback, timers callback và setImmediate()).
  • Check: xử lý hàm callback của setImmediate.
  • Close callbacks: thực thi các hàm callbacks cho các sự kiện close. Ví dụ: socket.on("close").

Giữa mỗi lần lặp của Event Loop, node.js sẽ kiểm tra xem nó có đang đợi bất kỳ I/O không đồng bộ hoặc timers nào không và thoát nếu không còn gì.

Chi tiết các pha (phase) của Event Loop

Timers

Một timers (bộ đếm thời gian) chỉ định ngưỡng mà sau đó một hàm callback có thể được thực hiện. Hàm callback của timers sẽ chạy sớm nhất có thể sau khi lượng thời gian được chỉ định trôi qua. Tuy nhiên, chúng cũng có thể bị delay trong một khoảng thời gian nào đó.

Lưu ý: Về mặt kỹ thuật, poll kiểm soát khi timers được thực thi.

Ví dụ: Giả sử chúng ta thiết lập một hàm setTimeout() được thực thi sau 100ms, sau đó chạy một hàm someAsyncOperation thực hiện việc đọc một file không đồng bộ mất 95ms:

const fs = require('fs');

function someAsyncOperation(callback) {
  // giả sử đọc file mất 95ms
  fs.readFile('/path/to/file', callback);
}

const timeoutScheduled = Date.now();

setTimeout(() => {
  const delay = Date.now() - timeoutScheduled;

  console.log(`${delay}ms`);
}, 100);

// hàm someAsyncOperation mất 95ms để hoàn thành
someAsyncOperation(() => {
  const startCallback = Date.now();

  // vòng lặp sẽ làm delay 10ms...  
  while (Date.now() - startCallback < 10) {
    // do nothing
  }
});

Khi Event Loop bước vào giai đoạn poll, nó có một hàng đợi trống (fs.readFile() chưa hoàn thành), vì vậy nó sẽ đợi số ms còn lại cho đến khi đạt đến ngưỡng của bộ định thời sớm nhất. Trong khi chờ 95 ms vượt qua, fs.readFile() đọc xong và hàm callback của nó mất 10ms để hoàn thành sẽ được thêm vào hàng đợi của poll và được thực thi. Khi hàm callback thực thi xong, không còn callback nào trong hàng đợi, do đó Event Loop sẽ thấy rằng ngưỡng của bộ định thời sớm nhất đã đạt đến sau đó kết thúc lại giai đoạn bộ định thời để thực hiện lệnh gọi lại của bộ định thời. Trong ví dụ này, bạn sẽ thấy rằng tổng thời gian trễ giữa bộ đếm thời gian được lập lịch và cuộc gọi lại của nó được thực thi sẽ là 105ms.

Lưu ý: Để ngăn giai đoạn thăm dò làm đói vòng lặp sự kiện, libuv (thư viện C triển khai Event Loop của node.js) cũng có giá trị tối đa (phụ thuộc vào hệ thống) trước khi nó dừng polling nhiều sự kiện hơn.

Pending callbacks

Giai đoạn này thực hiện các hàm callback đối với một số hoạt động của hệ thống, chẳng hạn như các loại lỗi TCP. Ví dụ: nếu socket TCP nhận được ECONNREFUSED khi cố gắng kết nối, một số hệ thống *nix muốn đợi để báo lỗi. Nó sẽ được đưa vào hàng đợi này để chờ được thực thi.

Poll

Poll có hai chức năng chính:

  • Tính toán thời gian nó sẽ chặn và thăm dò các sự kiện I/O, sau đó:
  • Xử lý các sự kiện trong hàng đợi poll

Khi Event Loop bước vào giai đoạn poll và không có các callback của timers nào, một trong hai trường hợp sau sẽ xảy ra:

  • Nếu hàng đợi poll không trống, Event Loop sẽ lặp lại qua các hàm callback của nó và thực hiện lần lượt chúng cho đến khi hàng đợi hết hoặc đạt đến giới hạn của hệ thống.
  • Nếu hàng đợi poll trống, một trong hai trường hợp nữa sẽ xảy ra:
    • Nếu các tập lệnh đã được lên lịch trước bởi setImmediate(), Event Loop sẽ kết thúc giai đoạn poll và tiếp tục đến giai đoạn check để thực thi các tập lệnh đã được lên lịch đó.
    • Nếu các tập lệnh chưa được lên lịch trước bởi setImmediate(), Event Loop sẽ đợi các hàm callbacks được thêm vào hàng đợi, sau đó thực thi chúng ngay lập tức.

Khi hàng đợi poll trống, Event Loop sẽ kiểm tra xem có bộ đếm thời gian nào đạt đến ngưỡng được thực thi. Nếu một hoặc nhiều cái đã sẵn sàng, Event Loop sẽ quay trở lại giai đoạn timers để thực hiện các hàm callbacks đó.

Check

Giai đoạn này cho phép chúng ta thực hiện các hàm callbacks ngay sau khi giai đoạn poll hoàn thành. Nếu giai đoạn poll đang có hàng đợi trống và có các tập lệnh đã được lên lịch trước bởi setImmediate(), Event Loop có thể tiếp tục đến giai đoạn này thay vì phải đợi.

setImmediate() là một bộ đếm thời gian đặc biệt chạy trong một giai đoạn riêng biệt của Event Loop. Nó sử dụng một API libuv để lập lịch các hàm callbacks thực thi sau khi giai đoạn poll hoàn thành.

Nói chung, khi các đoạn mã được thực thi, Event Loop cuối cùng sẽ đến giai đoạn poll - nơi nó sẽ đợi các kết nối đến, request, v.v... Tuy nhiên, nếu một hàm callback đã được lên lịch bởi setImmediate() và giai đoạn poll vào trạng thái nhàn rỗi, nó sẽ kết thúc và tiếp tục đến giai đoạn check hơn là chờ đợi các sự kiện của poll.

Close callback

Nếu một socket hoặc handle bị đóng đột ngột (ví dụ socket.destroy()), sự kiện 'close' sẽ được phát ra trong giai đoạn này. Nếu không, nó sẽ được phát ra thông qua process.nextTick().

Tổng kết

Event Loop của node.js được triển khai bằng libuv bao gồm 6 pha với mỗi pha xử lý một phần công việc riêng biệt. Khi đã biết được điều đó, chúng ta sẽ giải thích được thứ tự ưu tiên thực hiện các hàm callback của một số hàm như setTimeout, setImmediate, hay process.nextTick. Về thứ tự ưu tiên cũng như lợi ích của chúng, tôi sẽ có một bài viết khác để làm rõ vấn đề này. Hẹn gặp lại các bạn!

Cao cấp
Hello

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!

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!

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
Ẩn danh5 tháng trước
Chơi dịch từ nodejs à, có hiểu k mà viết bài v cha
Trả lời
Avatar
Xuân Hoài Tống5 tháng trước
Đúng là bài này mình dịch từ nodejs, từ khá lâu rồi và chưa có cập nhật gì thêm. Mình sẽ cố gắng hoàn thiện bài viết hơn nữa.