+6

Xử lí không đồng bộ trong javascript

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 nowlater trong khi xử lí song song thì xảy ra cùng lúc.

Những công cụ để xử lí song song là processthread. 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 XY, 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 XY, 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

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí