Kiến trúc Node.js - Single thread, Call stack, I/O đồng bộ và bất đồng bộ trong Node.js

Kiến trúc Node.js - Single thread, Call stack, I/O đồng bộ và bất đồng bộ trong Node.js

Tin ngắn hàng ngày dành cho bạn
  • CloudFlare đã giới thiệu tính năng pay per crawl để tính phí cho mỗi lần AI "cào" dữ liệu trên trang web của bạn. Là sao ta 🤔?

    Mục đích của SEO là giúp các công cụ tìm kiếm nhìn thấy trang web. Khi người dùng tìm kiếm nội dung mà có liên quan thì nó hiển thị trang web của bạn ra kết quả tìm kiếm. Điều này gần như là đôi bên cùng có lợi khi Google giúp nhiều người biết đến trang web hơn, còn Google thì được nhiều người dùng hơn.

    Bây giờ cuộc chơi với các AI Agents thì lại khác. AI Agents phải chủ động đi tìm kiếm nguồn thông tin và tiện thể "cào" luôn dữ liệu của bạn về, rồi xào nấu hay làm gì đó mà chúng ta cũng chẳng thể biết được. Vậy đây gần như là cuộc chơi chỉ mang lại lợi ích cho 1 bên 🤔!?

    Nước đi của CloudFlare là bắt AI Agents phải trả tiền cho mỗi lần lấy dữ liệu từ trang web của bạn. Nếu không trả tiền thì tôi không cho ông đọc dữ liệu của tôi. Kiểu vậy. Hãy chờ thêm một thời gian nữa xem sao 🤓.

    » Xem thêm
  • Lúc khái niệm "Vibe Code" bùng nổ mình cũng tò và tìm hiểu xem nó là gì. Hoá ra là chỉ cách lập trình mới: Lập trình viên ra lệnh và để cho LLM tự viết mã. Sau đó là hàng loạt các bài viết nói về cách họ đã xây dựng ứng dụng mà không cần phải viết một dòng mã nào, hoặc 100% là do AI viết...

    Mình không có ý kiến gì vì mỗi người một sở thích. Nhưng nếu tiếp xúc với nhiều thông tin như vậy thì ít nhiều thế hệ lập trình viên mới sẽ "ám ảnh". Khi làm việc với ngôn ngữ lập trình, chúng ta đang tiếp xúc ở bề nổi rồi. Đằng sau đó còn nhiều lớp khác che giấu sự phức tạp. Ví dụ biết viết JavaScript nhưng có biết nó chạy như thế nào không 🤔? Trên thực tế bạn chẳng cần phải biết nó chạy như thế nào mà chỉ cần biết cú pháp là viết được chương trình chạy ngon ơ.

    LLMs giờ đây lại thêm một lớp ảo hoá cho việc viết mã. Tức là nơi chúng ta không cần trực tiếp viết mà là ra lệnh. Làm việc sẽ nhanh hơn nhưng khi gặp vấn đề thì nhiều khả năng phải vận dụng kiến thức của tầng thấp hơn để giải quyết.

    Mình dùng Cursor, nhưng tính năng thích nhất và dùng nhiều nhất là Autocomplete & Suggestions. Thi thoảng cũng dùng Agents để bảo nó viết tiếp đoạn mã đang dở, thường thì nó làm rất tốt. Hoặc khi gặp lỗi thì hỏi, có lúc giải quyết được, lúc thì không. Nhìn chung nó đang làm thay nhiệm vụ của Google & Stack Overflow, giúp tiết kiệm thời gian 😆

    LLMs như một cuốn bách khoa toàn thư rất khủng khiếp. Hỏi gì cũng biết, cũng trả lời được nhưng có một sự thật là nó chỉ là mô hình đoán chữ (đoán tokens). Thế nên nếu vấn đề phổ biến thì nó sẽ làm rất tốt, nhưng vấn đề ít phổ biến hơn thì nó lại rất tệ, hoặc thậm chí là đưa ra thông tin sai lệch, nhiễu... Tóm lại, cần phải biết cách khai thác thông tin, mà để biết thì buộc người dùng phải có một lượng kiến thức nhất định, tránh rơi vào cái bẫy thiên kiến uy quyền (tin tưởng tuyệt đối vào ai đó) hoặc thiên kiến xác nhận (xác nhận niềm tin sẵn có bằng cách chỉ tìm bằng chứng xác nhận niềm tin đó).

    Tại thấy bài viết này nên lại nổi hứng viết vài dòng 🤓 Why I'm Dialing Back My LLM Usage

    » Xem thêm
  • Tiếp tục cập nhật vụ kiện giữa nhóm Deno và Oracle về cái tên JavaScript: Có vẻ như Deno đang yếu thế vì toà án đã bác bỏ đơn khiếu nại của nhóm Deno. Tuy nhiên trong tháng 8, họ (Oracle) phải có trách nhiệm giải trình từng lý do, thừa nhận hoặc phủ nhận những cáo buộc mà nhóm Deno trình ra trong vụ kiện.

    JavaScript™ Trademark Update

    » Xem thêm

Vấn đề

C là ngôn ngữ lập trình được nhiều người biết đến khi bắt đầu theo đuổi sự nghiệp. C đơn giản để tiếp cận nhưng sẽ trở thành "cơn ác mộng" cho những ai muốn dấn sâu hơn vào nó. Lập trình cũng giống như cách mà bạn đang cố gắng giải thích vấn đề gì đó với người khác, nhưng ở đây là giải thích với máy tính. Cách để tạo ra một chương trình là diễn giải điều muốn làm theo từng bước, từng dòng. Sau đó máy tính sẽ chạy chương trình theo thứ tự đã viết. Tức là mã được chạy lần lượt theo thứ tự từ trên xuống dưới, từ trái qua phải, xử lý xong đoạn mã này rồi mới đến đoạn mã tiếp theo.

Ví dụ chương trình in ra hai từ Hello World cách nhau 2 giây trong C.

#include <stdio.h>
#include <unistd.h>

int main() {
  printf("Hello");
  sleep(2);
  printf("World");

  return 0;
}

Thoạt nhìn vào ai cũng có thể đoán được đầu tiên đoạn mã trên in ra Hello, sau khi gặp hàm sleep nó dừng 2 giây rồi tiếp tục in ra World. Mã chạy như vậy gọi là mã đồng bộ.

Vậy nhưng trong JavaScript/Node.js cần phải làm quen với khái niệm bất đồng bộ, có những hàm được thiết kế để chạy đồng bộ, cũng có hàm được thiết kế bất đồng bộ để tối ưu hiệu suất. Bất đồng bộ tức là đoạn mã đó chưa trả về kết quả ngay lập tức mà được trả về ở một thời điểm nào đó trong tương lai.

Hãy nhìn vào ví dụ dưới đây và đoán xem kết quả cuối cùng là gì?

setTimeout(function() {
  console.log("Hello");
}, 0);

console.log("World");

Đoạn mã trên in ra World Hello thay vì Hello World, mặc dù dòng console.log("Hello") được viết trước. Bởi vì setTimeout là một hàm bất đồng bộ, kết quả không có ngay lập tức nên đoạn mã tiếp tục chạy và từ World được in ra trước.

Tại sao JavaScript/Node.js cần phải bất đồng bộ? Muốn biết được lý do, trước tiên cần phải biết nguyên lý hoạt động của nó.

Single Thread

Bạn đã nghe nhiều về JavaScript/Node.js chỉ có một luồng. Điều này ám chỉ mã chỉ được thực thi trong một luồng duy nhất. Nếu như vậy thì làm sao nó có thể xử lý hàng ngàn yêu cầu cùng lúc chỉ với một luồng? Tưởng tượng một máy chủ API có một điểm cuối (endpoint) mất 5 giây để trả về kết quả, trong khi có rất nhiều yêu cầu gửi đến? Vì Node.js chỉ có một luồng nên các yêu cầu sau phải đợi yêu cầu trước hoàn thành thì mới được xử lý!? Nếu vậy thì chẳng phải là quá tệ so với một ngôn ngữ phía máy chủ hay sao?

Thật may điều này không xảy ra vì JavaScript/Node.js được trang bị cơ chế xử lý bất đồng bộ. Trong Node.js tồn tại những hàm đồng bộ và không đồng bộ. Các câu lệnh cơ bản như khai báo biến, các phép tính +-x:, if else, switch case, các vòng lặp, JSON.parse, các hàm trong Node.js được kết thúc bằng chuỗi sync như readFileSync, gzipSync... được thực hiện đồng bộ. Còn lại hầu hết các hàm được thiết kế theo kiểu bất đồng bộ như readFile, gzip... các yêu cầu http... để nhằm mục đích không chặn vòng lặp sự kiện.

Call Stack

Call stack là ngăn xếp cuộc gọi để thực thi các lệnh JavaScript. Mã viết ra muốn chạy được cần phải đưa vào call stack. Vì là một stack nên nó tuân theo tắc vào sau ra trước (Last In First Out). Call stack là nơi đảm bảo thứ tự thực hiện chương trình.

Để làm rõ cách call stack hoạt động. Hãy xem một ví dụ về đoạn mã chuyển đổi độ C sang độ F dưới đây.

const add = (a, b) => a + b;
const multiply = (a, b) => a * b;

const addCofficient = (val) => multiply(val, 1.8);
const addConst = (val) => add(val, 32);

const convertCtoF = (val) => {
  let result = val;
  result = addCofficient(result);
  result = addConst(result);
  return result;
};

convertCtoF(100);

Trong ví dụ trên cuối cùng hàm convertCtoF được gọi. convertCtoF gọi hai hàm addCofficientaddConst. Call stack đảm bảo thứ tự thực hiện chương trình bằng cách sắp xếp hàm được gọi theo ngăn xếp.

Callstack thực hiện chương trình

Chúng ta có thể thấy convertCtoF được đẩy vào call stack đầu tiên, sau đó là hàm addCofficient. Trong addCofficient gọi hàm multiply nên nó tiếp tục được đẩy vào trên đầu của ngăn xếp. Khi không còn hàm nào bên trong, call stack bắt đầu thực thi chương trình. Call stack còn có một vai trò quan trọng trong việc xác định vị trí lỗi. Nếu xảy ra lỗi, trình báo lỗi hiển thị được Error Stack Trace tức là vị trí của lỗi. Bởi vì các hàm được đưa vào call stack theo thứ tự nên trình báo lỗi dễ dàng truy vết được vị trí của chúng ở đâu trong chương trình.

Hãy sửa lại hàm addConst bằng các đổi tham số thứ hai trong hàm add thành một biến không tồn tại.

const addConst = (val) => add(val, number);

Chạy chương trình, một lỗi bắn ra, bao gồm nguyên nhân và vị trí gây ra lỗi.

ReferenceError: number is not defined
   at addConst:5:32
   at convertCtoF:10:12
   at eval:14:1

Thông điệp này có ý nghĩa là number chưa được khai báo, ở dòng 5, bắt đầu từ cột 32, ở trong hàm convertCtoF ở dòng 10, bắt đầu từ cột 12...

Điều gì xảy ra nếu một hàm mất nhiều thời gian xử lý được đưa vào call stack? Giả sử addConst mất 5 giây để thực hiện thì nó chiếm 5 giây của call stack trước khi được giải phóng. Đó là lý do chúng ta cần các hàm bất đồng bộ. Vậy khi gặp hàm bất đồng bộ, call stack xử lý chúng như thế nào? Để hiểu được điều đó trước tiên chúng ta cần tìm hiểu về I/O.

Tác vụ I/O

Tác vụ I/O là những công việc có liên quan đến đọc/ghi dữ liệu, tệp tin, hoặc liên quan đến mạng như truy vấn http, socket... I/O xuất hiện ở khắp mọi nơi trong lập trình máy chủ. Một cách hình dung đơn giản là truy vấn cơ sở dữ liệu cũng được coi là tác vụ I/O.

Trong Node.js, I/O gồm có hai loại là đồng bộ và không đồng bộ.

I/O đồng bộ

Xem xét ví dụ đọc nội dung tệp dưới đây.

const pdf = fs.readFileSync(file.pdf);
console.log("pdf size", pdf.size);
const doc = fs.readFileSync(file.doc);
console.log("doc size", doc.size);

Nhớ lại các hàm kết thúc bằng sync thường là đồng bộ. Đọc nội dung một tệp là tác vụ I/O tốn nhiều thời gian để xử lý. readFileSync là một hàm chạy đồng bộ, file.pdf đọc xong thì file.doc mới đến lượt. Thời gian xử lý của 2 hàm được mô tả như hình dưới đây.

Quá trình đọc file

Thời gian đọc file.pdf là 3ms, file.doc là 3ms. Thời gian in kết quả ra màn hình là 2ms, tổng thời gian chúng ta phải đợi tất cả công việc hoàn thành là 6ms.

6ms là rất nhanh. Nhưng hãy tưởng tượng kích thước của các tệp tin tăng lên kéo theo thời gian đọc tăng lên thì sao? Lúc đó nhiều khả năng call stack không thể thực hiện thêm đoạn mã nào cả. Chương trình lúc đó chạy theo thứ tự đọc -> in -> đọc -> in... một cách tuần tự.

I/O không đồng bộ

Bây giờ hãy sửa lại đoạn mã trên một chút, bằng cách thay hàm readFileSync thành readFile:

const pdf = fs.readFile(file.pdf);
console.log("pdf size", pdf.size);
const doc = fs.readFile(file.doc);
console.log("doc size", doc.size);

readFile là một hàm không đồng bộ. Hàm không đồng bộ là hàm có kết quả không trả về ngay lập tức, mà nó sẽ được trả ra ở một thời điểm nào đó. Nếu chạy đoạn mã trên bạn có thể thấy kết quả là một cái gì đó giống như thế này:

pdf size undefined
doc size undefined

Đó là do kết quả của đọc tệp tin không trả về ngay lập tức, vì thế mọi nỗ lực truy cập vào thuộc tính size sẽ không mang lại kết quả nào cả. Kết quả của hàm bất đồng bộ thường được trả về thông qua một hàm gọi lại, gọi là callback. Từ ES6 trở đi, chúng ta có thêm khái niệm Promise, kết quả của hàm bất đồng bộ còn được trả về trong then của Promise.

fs.readFile(file.pdf)
  .then(pdf => console.log("pdf size", pdf.size));

fs.readFile(file.doc)
  .then(doc => console.log("doc size", doc.size));

then được sử dụng để nhận kết quả tương tự callback của hàm bất đồng bộ. Nếu không sử dụng then thì dùng hàm callback để nhận kết quả như sau.

fs.readFile(file.pdf, function(err, pdf) {
  console.log("pdf size", pdf.size);
})

Thời gian xử lý giảm xuống đáng kể khi thay thế hàm readFileSync thành readFile. Thể hiện như ở sơ đồ dưới đây.

Đọc file bất đồng bộ

Kết luận

Node.js với kiến trúc đơn luồng (single thread) kết hợp cùng cơ chế bất đồng bộ đã giải quyết hiệu quả bài toán xử lý đồng thời mà không gây tắc nghẽn, một thách thức lớn đối với các hệ thống máy chủ truyền thống. Call stack đảm bảo thứ tự thực thi mã theo nguyên tắc vào sau ra trước (LIFO). Sự khác biệt giữa I/O đồng bộ và bất đồng bộ cũng được làm rõ: trong khi I/O đồng bộ chặn toàn bộ tiến trình cho đến khi tác vụ hoàn tất, I/O bất đồng bộ cho phép chương trình tiếp tục thực thi, giảm đáng kể thời gian chờ và tăng khả năng xử lý các yêu cầu lớn.

Ở bài viết tiếp theo, chúng ta hãy cùng nhau tìm hiểu xem JavaScript/Node.js xử lý bất đồng bộ như thế nào nhé.

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

Nội dung bình luận...