Xử lí không đồng bộ trong javascript
Bài đăng này đã không được cập nhật trong 8 năm
Xứ lí không đồng bộ (async) là 1 phần quan trọng trong javascript. Cách tiếp cận phổ biến với xử lí không đồng bộ là sử dụng các callback. Khác với việc thực hiện tuần tự các phép toán như trong 1 vòng for, có 1 khoảng thời gian chênh lệch nhất định giữa thời điểm giữa phần chương trình được chạy ngay lập tức và phần chương trình khác được gọi ra sau đó (callback).
Ví dụ như 1 lệnh ajax gửi request lên server và sau khi nhận được thành công data trả về từ server thì phần chương trình trong callback sẽ được thực hiện. Thời điểm mà callback được thực hiện không phải là ngay lập tức sau khi có request ajax mà có thể là 1 vài giây sau đó tùy thuộc vào tốc độ xử lí của server. Nếu coi thời điểm bắt đầu gọi là now thì thời điểm callback được thực hiện là 1 mốc thời gian later nào đấy và có 1 sự chênh lệch (gap) giữa now và later. Để hiểu rõ hơn cách mà javascript xử lí không đồng bộ như thế nào, chúng ta hãy chia nhỏ chương trình thành những phần nhỏ và xem xét cách chúng vận hành.
A Program in Chunks
Chúng ta chia chương trình javascript thành các đoạn (chunk) theo thời điểm mà chúng được thực thì (now or later). Một trong những vấn đề thường gặp của những người mới lập trình với javascript là cảm giác phần later sẽ chạy ngay lập tức sau now và do đó bỏ qua các khối kiểm tra cần thiết.
var data = ajax( "http://some.url" );
console.log(data);
Khi nhìn qua đoạn code trên thì do thói quen nhiều người không để ý rằng kết quả data được trả về nằm trong phần xử lí không đồng bộ nên khả năng rất lớn là lệnh log lại giá trị của data được thực thi trước khi có data trả về từ câu lệnh ajax. Chúng ta gọi câu lệnh ajax tại thời điểm now nhưng kết quả data trả về là 1 thời điểm later. Giải pháp đơn giản cho vấn đề này là đặt câu lệnh log vào trong callback khi request ajax hoàn thành
ajax( "http://some.url", function callbackFunction(data){
console.log( data );
});
Async Console
console.log
là lệnh rất hay được sử dụng đặc biệt là trong trường hợp dùng để debug.
Tuy nhiên trên thực tế trên 1 vài trình duyệt và trong 1 số điều kiện, lệnh console.log
không đưa ra kết quả output ngay lập tức. Ví dụ như khi I/O quá chậm lệnh console này có thể
chạy không đồng bộ và chúng ta không biết chính xác thời điểm lệnh này được thực thi là khi nào.
var a = {
index: 1
};
console.log(a);
a.index++;
Trong trường hợp này sẽ có lúc kết quả output của console.log
không phải là 1 mà có thể là 2.
Event Loop
Javascript không chạy độc lập mà chạy trong 1 hosting enviroment
, thường thì là trình duyệt
. Hiện nay Javascript cũng có thể hoạt động trong môi trường server (Node.js).
Khi Javascript xử lí các đoạn code của chương trình, có 1 cơ chế được sử dụng đó là event loop
.
Ví dụ khi chương trình javscript chạy 1 request ajax để lấy data từ server và sau đó thực hiện 1 callback
trong phần response trả về, JS engine sẽ nói với hosting environment sẽ trì hoãn việc thực thi callback ngay lập tức mà
hãy gọi nó khi hoàn thành xong công việc lấy data từ server.
Vậy event loop
là gì. Hãy xem xét đoạn code mô phỏng dưới đây
var eventLoop = [];
var event;
while (true) {
if (eventLoop.length > 0) {
event = eventLoop.shift();
try {
event();
}
catch (err) {
reportError(err);
}
}
}
Vòng while vô hạn sẽ được thực hiện. Ở đây ta có thể hiểu event loop
như 1 queue.
Tại mỗi lần lặp lại trong vòng while
thì queue này được check xem có event nào trong đó hay không,
nếu có thì lấy event đó ra khỏi queue và thực thi nó. Khi chạy lệnh setTimeout
thì
cho callback trong lệnh này sẽ được cho vào event loop và nếu như trong event loop đang có
các callback khác thì callback của setTimeout
sẽ phải chờ này tới lượt. Điều đó có nghĩa là
nếu bạn set interval cho lệnh setTimeout
là 1s thì không có nghĩa là sau 1s callback của nó sẽ được gọi mà
chúng ta sẽ chỉ có thể đảm bảo là callback đó sẽ không được chạy khi chưa quá 1s
Parallel Threading
Chúng ta khá dễ nhầm lẫn giữa 2 khái niệm async
(không đồng bộ) và parallel
(song song).
Xử lí không đồng bộ ám chỉ về việc chênh lệch thời gian giữa now
và later
trong khi xử lí song song thì xảy ra cùng lúc.
Những công cụ để xử lí song song là process
và thread
. Trong event loop
các task được xử lí tuần tự và không cho phép truy cập song song.
Khi xử lí song song, việc thực thi xen kẽ giữa các thread xảy ra ở level thấp.
function later() {
answer = answer * 2;
console.log( "Answer:", answer );
}
Ví dụ lệnh answer = answer * 2
thực ra bao gồm các bước load giá trị hiện tại của answer
, thực hiện tính toán rồi sau đó mới lưu giá trị mới vào answer
Việc xử lí song song có thể gây ra các ảnh hưởng không ngờ đến.
var a = 20;
function foo() {
a = a + 1;
}
function bar() {
a = a * 2;
}
ajax( "http://some.url.1", foo );
ajax( "http://some.url.2", bar );
Trong Javascript khi xử lí single-thread thì giá trị a trả về sau khi thực hiện 2 callback foo, bar sẽ là 42 hoặc 41 tùy thuộc xem callback nào được gọi trước. Bây giờ nếu như Javascript thực hiện xử lí song song hãy xem có vấn đề gì xảy ra. Trước hết chúng ta chia nhỏ các công việc được thực hiện trong foo và bar Gọi X và Y là các địa chỉ vùng nhớ tạm thời
foo():
1a. load giá trị của a tại X
1b. lưu 1 vào Y
1c. cộng X và Y, rồi lưu kết quả vào X
1d. lưu giá trị tại X cho a
bar():
2a. load giá trị của a tại X
2b. lưu 1 vào Y
2c. nhân X và Y, rồi lưu kết quả vào X
2d. lưu giá trị tại X cho a
Khi xử lí song song nếu quá trình thực thi như sau thì kết quả trả về sẽ là 44:
1a 2a 1b 2b 1c 1d 2c 2d
và nếu thứ tự thực hiện khác đi chúng ta lại có 1 kết quả khác
1a 2a 2b 1b 2c 1c 1d 2d
kết quả sẽ là 21
Có thể thấy xử lí song song với thread là khá phức tạp khi mà kết quả trả về là rất khác nhau tùy theo thứ tự thực hiện các phép toán.
Mặc dù khi xử lí single thread kết quả trả về vẫn là không đồng nhất tuy nhiên nó nằm ở mức function (event) order tức là kết quả chỉ phụ thuộc vào thứ tự xảy ra của event chứ không đến mức statement level.
Noninteracting
Khi thực hiện xử lí không đồng bộ nếu chúng ta đảm bảo việc các callback thực hiện các công việc riêng rẽ, độc lập, không có tương tác gì với nhau thì cho dù thứ tự thực thi của các callback có khác nhau đi chăng nữa cũng không ảnh hưởng gì đến kết quả.
var res = {};
function foo(results) {
res.foo = results;
}
function bar(results) {
res.bar = results;
}
ajax( "http://some.url.1", foo );
ajax( "http://some.url.2", bar );
Trong trường hợp này thì cho dù foo hay bar chạy trước thì cũng không ảnh hưởng gì đến kết quả
Interaction
Việc đảm bảo các callback thực hiện các công việc riêng rẽ, độc lập sẽ giúp chương trình chạy ổn định nhưng trên thực tế nhiều lúc chúng ta phải xử lí các callback có tinh tương tác với nhau (ví dụ như cùng thay đổi kết cấu DOM chẳng hạn). Việc các callback này cùng chia sẻ 1 tài nguyên nào đó (1 biến global chẳng hạn) có thể làm chương trình bị xử lí sai.
var a, b;
function foo(x) {
a = x * 2;
baz();
}
function bar(y) {
b = y * 2;
baz();
}
function baz() {
console.log(a + b);
}
ajax( "http://some.url.1", foo );
ajax( "http://some.url.2", bar );
Trong ví dụ này khả năng cao là function baz sẽ được gọi quá sớm khi mà 1 trong 2 function bar hoặc foo chưa hoàn thành dẫn đến 1 trong 2 biến a hoặc b bị undefined. Cách khắc phục vấn đề này là thêm các block check điều kiện.
var a, b;
function foo(x) {
a = x * 2;
if (a && b) {
baz();
}
}
function bar(y) {
b = y * 2;
if (a && b) {
baz();
}
}
function baz() {
console.log( a + b );
}
ajax( "http://some.url.1", foo );
ajax( "http://some.url.2", bar );
Với cách xử lí như vậy, chúng ta sẽ đảm bảo chỉ thực hiện baz khi cả 2 biến a và b đều đã nhận được giá trị
Kết luận
- Javascript chia chương trình thành các đoạn code, có phần sẽ chạy ngay lập tức (now) có phẫn sẽ chạy vào 1 thời điểm khác sau đó.
- Javascript sử dụng event loop để quản lí các event, bản chất là 1 queue và sẽ thực hiện lần lượt cho đến hết các event trong queue đó.
- Việc đảm bảo các event hoạt động riêng rẽ độc lập sẽ giúp kết quả trả về không bị phụ thuộc vào thứ tự thực hiện của các event.
- Khi nhiều event cùng chia sẻ 1 tài nguyên nào đó cần có các cơ chế kiểm tra để đảm bảo kết quả trả về chính xác
All rights reserved