Hướng dẫn non blocking nodejs - nodejs không chặn

Có hai yếu tố mạnh mẽ nhất làm Node.js thông dụng trong thời gian thật ngắn: Cả server và browser có thể cùng điều hành bởi 1 ngôn ngữ lập trình, JavaScript. Yếu tố này chỉ là sự việc tình cờ khi Ryan Dahl chọn JavaScript làm ngôn ngữ lập trình để viết Node.js, anh ta không có ý là cả hai phía server và browser phải dùng cùng ngôn ngữ. Yếu tố thứ hai, non-Blocking I/O and Asynchronous Events, và đây mới là động lực duy nhất thúc đẩy Ryan Dahl tạo ra Node.js.

Non-Blocking I/O, một cách đơn giản, có nghĩa những công việc gì có dính dáng về input output không thể có quyền ngăn chặn những công việc khác. IO có nghĩa dữ kiện chạy đi chạy lại từ một nơi này đến một nơi khác như từ: bộ nhớ vào ổ cứng hay ngược lại; server đến browser và ngược lại, vân vân. Trong các loại công việc của máy tính, chung chung, IO là chậm nhất. Chậm hơn rất nhiều so với tính toán. Vì lý do đó nếu IO được điều hành làm sao mà không bê trễ những công việc khác của máy tính thì cả hệ thống tính toán sẽ bớt trì trệ, hay nói cách khác là nhanh hơn. Điều này không phải là khám phá mới mà đã được làm trong quá khứ, tuy nhiên chỉ là đắp vá tạm thời và chưa bao giờ ở cả một hệ thống tính toán như node.js.

Asynchronous Events có thể chung chung là mọi biến có đều phải theo một thứ tự trước sau. Có nghĩa khác với Synchronous (đồng bộ) hay Parallel (song song).

Hai ý tưởng này hợp lại có nghĩa sự kiện sảy ra theo thứ tự trước sau như đã xếp hàng trong trong lập trình; tuy nhiên nếu có sự kiện nào phải chờ đợi IO thì cứ chờ nhưng không vì cớ đó mà ngăn chặn những sự kiện khác trong hàng

Cô Hoa và anh Tư Ếch

Lấy một thí dụ đời thường. Cô Hoa là chủ một tiệm tạp hóa. Công việc hằng ngày là sắp xếp hàng hóa, thu hàng vào, gửi hàng đi, và việc quan trọng hơn cả là tiếp khách hàng, Tiệm nhỏ nhưng lúc nào cũng đầy công việc và cô Hoa lúc nào cũng bận bịu không vì việc này thì cũng việc khác. Tiệm buôn bán khá phần lớn nhờ váo phong cách đối đãi niềm nở với khách hàng của cô Hoa. Có một lần cô Hoa phải đi xa và phải mướn người trông tiệm, anh Tư Ếch, có tiếng là siêng năng cần mẫn và nhất nhất vâng lời chủ dặn (đặc tính của hệ thống vi tính). Trước đó, để sửa soạn, cô viết xuống giấy (lập trình) những việc phải làm trong ngày theo thứ tự (asychronous) mà cô vẫn làm bao nhiêu năm nay cho anh Tư và dặn cứ làm theo thứ tự thì sẽ trôi chẩy. Cô Hoa đi công việc nhưng rất yên tâm, thầm cảm ơn vì mướn được người như anh Tư Ếch. Một tuần sau cô Hoa về thì thấy tiệm tùng ngăn nắp sạch sẽ nhưng khi kiểm chi thu thì hình như chẳng được đồng nào. Cô không hiểu tại sao và khi hỏi anh Tư,

"Cả tuần em đi vắng tiệm không có khách à?" "Có chứ, mấy ngày đầu đông lắm nhưng những ngày sau họ không đến nữa," anh đáp. "Ủa! Tại sao vậy?" "Khách đến đông nhưng không ai tiếp. Tui thì mắc dọn dẹp và xếp hàng hóa."
"Có chứ, mấy ngày đầu đông lắm nhưng những ngày sau họ không đến nữa," anh đáp.
"Ủa! Tại sao vậy?"
"Khách đến đông nhưng không ai tiếp. Tui thì mắc dọn dẹp và xếp hàng hóa."

Cô Hoa mới vỡ lẽ rằng trong lập trình của cô, tiếp khách là việc sau cùng. Anh Tư hiểu rằng phải bày biện, lau chùi, sắp xếp hàng hóa (IO) xong rồi thì mới đến tiếp khách (blocking). Khách đến không ai tiếp thì khách không đến nữa. Trong lập trình, cô quên dặn rằng nếu có khách thì phải bỏ dở việc lau chùi mà tiếp khách (non-Blocking IO). Chuyện có vẻ nhạt nhẽo và phi lý nhưng cốt ý để trình bày hiện tượng asynchronously blocking. Trong trường hợp này cô Hoa là lập trình viên và anh Tư Ếch là hệ thống vi tính.

Đại khái lập trình của cô Hoa như dưới đây. Hàm shopKeeping bao gồm những hàm chi tiết khác. Hàm tiepKhach nằm dưới cùng. Trong một môi trường single thread aschynchronous hàm liệt kê trên cao phải hoàn tất (không thể bỏ dở công việc đang làm được gọi là blocking) thì hàm liệt kê dưới đó mới được quan tâm. Anh Tư chưa làm xong những hàm trên nên việc tiepKhach chưa đến phiên mặc dù khách đông và đang chờ.

function shopKeeping() {
    moCuaTiem();
    donDepLauChui();
    kiemKeHangHoa();
    tiepKhach();
}
shopKeeping();

Kỳ tới cô Hoa lại có việc đi xa, chúng ta để xem cô Hoa sẽ có biện pháp gì và có còn mướn anh Tư nữa không.

Trong bài này mình giới thiệu hai đoạn mã nhỏ cũng khá lý thú để những post sau này mình sẽ dùng cho những thí dụ về I/O non-Blocking. Đoạn mã đầu tiên là một hàm nhỏ ghi lại thời điểm hiện tại đi cùng với đoạn text. Hàm này tiện dụng cho logging. Mình đặt tên file này là linhkien.js nhé.

function now(txt) {
    console.log(new Date().toISOString().replace(/T/, ' ').replace(/\..+/, '')+' '+txt);
}
now('> Hello World');

Khi chạy nó sẽ ra như thế này, cũng vui vui. Các bạn tự nghiệm hàm chạy làm sao nhé.

C:\nodeDev> node linhkien.js 2014-05-11 23:05:05 > Hello World C:\nodeDev>
2014-05-11 23:05:05 > Hello World
C:\nodeDev>

Hàm thứ hai là hàm wait. Hàm này chẳng làm gì cả chỉ looping around doing nothing cho một thời gian x mili giây đồng hồ nào đó. Hàm có tính cách blocking, có nghĩa khi hàm này chạy thì không có cái gì chạy cả mà phải chờ đến khi hàm này chạy xong. Các bạn nhớ không? Single thread á. Again, bạn tự nghiệm nhé.

function wait(miliSeconds) {
    var startTime = new Date().getTime();
    while (new Date().getTime() < startTime + miliSeconds);
}

Bạn hợp hai hàm này vào cùng 1 file như sau:

function now(txt) {
    console.log(new Date().toISOString().replace(/T/, ' ').replace(/\..+/, '')+' '+txt);
}

function wait(miliSeconds) {
    var startTime = new Date().getTime();
    while (new Date().getTime() < startTime + miliSeconds);
}

now('> Start to wait');
wait(5000);
now('> Finish waiting');

Khi chạy nó sẽ ra như thế này, cũng vui vui. Các bạn tự nghiệm hàm chạy làm sao nhé. Để ý mốc thời gian cách nhau 5 giây.

%(blue)[C:\nodeDev> node linhkien.js 2014-05-11 23:05:17 > Start to wait 2014-05-11 23:05:22 > Finish waiting C:\nodeDev>]
2014-05-11 23:05:17 > Start to wait
2014-05-11 23:05:22 > Finish waiting
C:\nodeDev>]

I/O non-Blocking: setTimeout

Trước khi đề cập đến setTimeout, mình chỉnh sửa hàm wait của mình chút đỉnh để dễ so sách nha. Cũng không gì nhiều, chỉ chêm vào một parameter. Parameter này là một hàm mình muốn chạy sau khi thời gian wait chấm dứt. Hàm gọi hàm mà mình đã đề cập ở post trước áh. Trong trường hợp này, hàm mình muốn chạy chỉ đơn giản là now với dòng chữ End of waiting. Nhìn đoạn mã bạn sẽ hiểu ngay.

function now(txt) {
    console.log(new Date().toISOString().replace(/T/, ' ').replace(/\..+/, '')+' '+txt);
}

function wait(fn, miliSeconds) {
    var startTime = new Date().getTime();
    while (new Date().getTime() < startTime + miliSeconds);
    fn();
}

now('> Start to wait');
wait(function(){now('> End of waiting');}, 5000);

Khi chạy nó sẽ ra như thế này, cũng vui vui. Để ý đến mốc giờ, start và end cách nhau 5 giây (hay là 5000 mili giây). Các bạn tự nghiệm hàm chạy làm sao nhé.

 > C:\nodeDev> node linhkien.js 2014-05-11 23:05:17 > Start to wait 2014-05-11 23:05:22 > End of waiting C:\nodeDev>
2014-05-11 23:05:17 > Start to wait
2014-05-11 23:05:22 > End of waiting
C:\nodeDev>

Bây giờ thì đến setTimeout. Hàm này trong core của Node.js. Về hình thức hàm này y chang như hàm wait, implementation thì lại khác. Trong khi wait is blocking, setTimeout lại là non-Blocking. Đầu tiên ta xem hai hàm này giống chỗ nào nha. Từ đoạn mã trên ta chỉ thay thế tên hàm wait với setTimeout (hàng cuối) và run. Kết quả cũng sẽ giống nhau.

function now(txt) {
    console.log(new Date().toISOString().replace(/T/, ' ').replace(/\..+/, '')+' '+txt);
}

function wait(fn, miliSeconds) {
    var startTime = new Date().getTime();
    while (new Date().getTime() < startTime + miliSeconds);
    fn();
}

now('> Start to wait');
setTimeout(function(){now('> End of waiting');}, 5000); // Thay wait bằng setTimeout

Khi chạy nó sẽ ra như thế này. Để ý đến mốc giờ, start và end cách nhau 5 giây (hay là 5000 mili giây) như trên.

 > C:\nodeDev> node linhkien.js 2014-05-11 23:12:11 > Start to wait 2014-05-11 23:12:16 > End of waiting C:\nodeDev>
2014-05-11 23:12:11 > Start to wait
2014-05-11 23:12:16 > End of waiting
C:\nodeDev>

Để thấy sự khác biệt giữ wait (blocking) và setTimeout (non-Blocking) mình chỉ việc kèm thêm một hành động nào đó ngay sau cùng và quan sát xem khi nào thì hành động này được run. Theo định nghĩa, với blocking, hành động cuối sẽ chạy sau khi thời gian wait chấm dứt. Ngược lại, với non-Blocking, hành động cuối sẽ bắt đầu ngay sau khi bắt đầu waiting. Mình sẽ thử cả hai để so sánh nha.

  • Thêm vào cuối đoạn mã now('> Start to do something else');
  • Hai đoạn mã dưới đây mình không hiển thị mã của 2 hàm now và wait cho dễ nhìn.
now('> Start to wait');
wait(function(){now('> End of waiting');}, 5000); 
now('> Start to do something else');

 > C:\nodeDev> node linhkien.js 2014-05-11 23:22:10 > Start to wait 2014-05-11 23:22:15 > End of waiting 2014-05-11 23:22:15 > Start to do something else C:\nodeDev>
2014-05-11 23:22:10 > Start to wait
2014-05-11 23:22:15 > End of waiting
2014-05-11 23:22:15 > Start to do something else
C:\nodeDev>

now('> Start to wait');
setTimeout(function(){now('> End of waiting');}, 5000); 
now('> Start to do something else');

 > C:\nodeDev> node linhkien.js 2014-05-11 23:23:20 > Start to wait 2014-05-11 23:23:20 > Start to do something else 2014-05-11 23:23:25 > End of waiting C:\nodeDev>
2014-05-11 23:23:20 > Start to wait
2014-05-11 23:23:20 > Start to do something else
2014-05-11 23:23:25 > End of waiting
C:\nodeDev>

Tóm tắt

  • Post này trình bày sự khác biệt giữa blocking và non-Blocking operation với mục đích nhấn mạnh non-Blockinghành xử thế nào. Thông hiểu khái niệm non-Blocking sẽ giúp chúng ta lập trình dễ dàng hơn với node.js.
  • Một điểm khác mình cũng muốn đề cập là callback. Cái hàm được gọi bởi hàm setTimeout là một callback function. Chung chung có thể hiểu như là, sau khi hết hạn timeout thì anh gọi cho tôi biết để tôi bắt đầu làm việc của tôi. Trong ví dụ này hết hạn timeout là event. Những events khác, thường là IO, có thể như file opened, file close, end of file, database connected, end of search, vân vân.

I/O non-Blocking: process.nextTick

Các posts trước mình đã viết hàm wait, hàm chẳng làm gì cả chỉ loop around và ngăn trở khởi động các hàm khác. Hàm không có giá trị thực tiễn, chỉ được dùng để dẫn nhập vào khái niệm non-Blocking & single-threaded, khái niệm quan trọng nhất của node.js. Mình đã đề cập đến hàm setTimeout để quan sát tương đồng và tương dị với hàm wait. setTimeout căn bản là một loại hàm non-Blocking dùng để hoãn lại một hoạt động nào đó đồng thời cứ để những hoạt động khác có xếp hàng trong thread (event queue) diễn tiến như thường lệ. Hàm setTimeout có 2 parameters. Parameter thứ nhất là một hàm mình muốn hoãn, parameter thứ hai là khoản thời gian hõan tính theo mili giây. Điều này dẫn đến câu hỏi: Nếu thời gian hõan là 0 mili giây thì sao nhỉ? Good question. Nếu thời gian hoãn là 0 thì hoạt động hoãn sẽ được đặt ngay cuối event queue. Có nghĩa ngay tại thời điểm setTimeout được gọi, hàm callback là event cuối cùng. Quan sát đoạn mã sau đây bạn sẽ thấy.

function now(txt) {
    console.log(new Date().toISOString().replace(/T/, ' ').replace(/\..+/, '')+' '+txt);
}

now('> Start to wait');
setTimeout(function(){now('> End of waiting');}, 0); 
now('> Start to do something else');

Khi chạy nó sẽ ra như thế này. Để ý đến mốc giờ, start và end giống nhau, chỉ là thứ tự trên dưới. Nên nhớ là giờ hiển thị ở đây là giây chứ không phải mili giây.

 > C:\nodeDev> node linhkien.js 2014-05-14 08:12:11 > Start to wait 2014-05-14 08:12:11 > Start to do something else 2014-05-14 08:12:11 > End of waiting C:\nodeDev>
2014-05-14 08:12:11 > Start to wait
2014-05-14 08:12:11 > Start to do something else
2014-05-14 08:12:11 > End of waiting
C:\nodeDev>

- process.nextTick

Trong core của node.js có hàm process.nextTick phản ảnh y chang như setTimeout(callback, 0). process.nextTick chỉ có 1 parameter là hàm callback, bởi lẽ process.nextTick sẽ đặt callback vào cuối event queue và parameter không cần thiết. Bạn thực tập đoạn mã dưới và ngiệm sét nhé.

now('> Start to wait');
process.nextTick(function(){now('> End of waiting');}); 
now('> Start to do something else');

 > C:\nodeDev> node linhkien.js 2014-05-14 08:12:18 > Start to wait 2014-05-14 08:12:18 > Start to do something else 2014-05-14 08:12:18 > End of waiting C:\nodeDev>
2014-05-14 08:12:18 > Start to wait
2014-05-14 08:12:18 > Start to do something else
2014-05-14 08:12:18 > End of waiting
C:\nodeDev>

- Tóm tắt

Ứng dụng của setTimeout và process.nextTick để viết mã non-Blocking trong lập trình node.js. Điều này dẫn đến câu hỏi: Nếu đã có setTimeout(callback, 0) thì việc gì phải làm thêm process.nextTick(callback)? Có phải là rắc rối tơ? Thật sư hai hàm có hai ứng dụng khác nhau, trong điều kiện thời gian hoãn là 0, hàm process.nextTick hữu hiệu hơn setTimeout rất nhiều. Mục đích của node.js là nhanh gọn lẹ, đó là lý do cho sự hiện hữu của process.nextTick. Mình sẽ khảo sát kỹ về cách viết và những ứng dụng trong tương lai khi có cơ hội. Ở thời điểm bắt đầu này mình lập lại lần nữanon-Blocking & single threaded là điểm đặc trưng của node.js.

  • Event queue: Trong một môi trường vi tính, nhiều sự kiện được sắp xếp theo thứ tự trước sau. Google dịch là hàng đợi sự kiện.
  • Single threaded: [Một môi trường vi tính,] sự kiện được xử lý theo chỉ một event queue.
  • non-Blocking: Một kiểu xử lý mà sự kiện đang xảy ra không dành độc quyền hiện hữu.
  • Asynchronous: (với hai hoặc nhiều vật thể hay biến cố) không cùng hiện hữu hay xảy ra trong cùng một thời gian. Suy ra, nếu không cùng một thời gian điều này đồng nghĩa với "chung chung là mọi biến có đều phải theo một thứ tự trước sau".
  • setTimeout: Một hàm trong core của node.js. Hàm này trì trệ hàm callback trong khoảng thời gian x mili giây.
  • process.nextTick: Một hàm trong core của node.js. Hàm này đặt hàm callback vào cuối event queue.