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ó.
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 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 addCofficient
và addConst
. 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.
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 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ộ.
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.
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ự.
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.
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é.
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 (0)