[JS] General asynchronous programming concepts

Thái Bình, chiều 30 Tết, trời âm u, se se lạnh …

Trong phần này, chúng ta sẽ tìm hiểu 1 số khái niệm quan trọng liên quan đến lập trình bất đồng bộ và cách trình duyệt và JavaScript xử lý nó.

Asynchronous

Bình thường, code của 1 chương trình chạy thẳng dọc và chỉ có 1 thứ diễn ra trong 1 thời điểm. Nếu 1 hàm dựa vào kết quả của hàm khác, nó sẽ phải chờ hàm khác kết thúc và trả về.

Điều này đôi khi gây ra trải nghiệm khó chịu với người dùng khi có các task mất thời gian. Nó cũng không khai thác hết sức mạnh của máy tính khi các máy tính hiện đại giờ đây đã có nhiều cores xử lý. Không có gì phải chờ đợi nữa khi bạn có thể để các task khác trên bộ xử lý khác và cho bạn biết khi nào nó hoàn thành. Điều này làm cho bạn có thể làm các việc khác trong cùng khoảng thời gian, và nó cơ bản gọi là lập trình bất đồng bộ (asynchronous programming). Nó phụ thuộc vào môi trường lập trình mà bạn đang sử dụng (web browers, truong trường hợp phát triển web) cung cấp cho bạn các API cho phép bạn chạy các task bất đồng bộ.

Blocking code

Kỹ thuật bất đồng bộ thực sự rất hữu ích, đặc biệt là trong lập trình web. Khi ứng dụng web của bạn chạy trên trình duyệt và nó thực thi một đoạn mã nặng, không trả về quyền kiểm soát cho trình duyệt, trình duyệt có thể bị đóng băng. Cái này gọi là blocking, trình duyệt chặn các yêu cầu xử lý tiếp theo từ người dùng và các tasks khác cho đến khi ứng dụng web trả về quyền kiểm soát các tiến trình.

Hãy nhìn 1 vài ví dụ bên dưới bạn sẽ hiểu blocking.

Ví dụ về simple-sync.html, chúng ta thêm event listener khi click 1 button, nó sẽ chạy một hoạt động tốn thời gian (tính toán 10 triệu ngày và sau đó ghi lại cái cuối cùng ra màn hình) và sau đó thêm một đoạn văn bản vào DOM:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const btn = document.querySelector("button");
btn.addEventListener("click", () => {
let myDate;
for (let i = 0; i < 10000000; i++) {
let date = new Date();
myDate = date;
}

console.log(myDate);

let pElem = document.createElement("p");
pElem.textContent = "This is a newly-added paragraph.";
document.body.appendChild(pElem);
});

Khi chạy ví dụ này, mở console của JavaScript sai khi click button - bạn sẽ nhận thấy rằng đoạn văn bản không xuất hiện cho đến khi ngày kết thúc sự tính toán và console đã được log lại.

Note: Ví dụ trên là vô nghĩa, nó chỉ là ví dụ mang tính chất minh họa

Trong ví dụ thứ 2 simple-sync-ui-blocking.html chúng tôi sẽ ví dụ một cái gì đó thực tế hơn 1 chút khi bạn lập trình web. Chúng ta sẽ chặn các tương tác người dùng khi đang rendering UI. Trong ví dụ này chúng ta sẽ có 1 buttons:

  • “Fill canvas” button, cái mà khi bạn click, sẽ điền <canvas> với 1 triệu vòng tròn xanh
  • “Click me for alert” khi click sẽ hiển thị 1 msg
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function expensiveOperation() {
for (let i = 0; i < 1000000; i++) {
ctx.fillStyle = "rgba(0,0,255, 0.2)";
ctx.beginPath();
ctx.arc(
random(0, canvas.width),
random(0, canvas.height),
10,
degToRad(0),
degToRad(360),
false
);
ctx.fill();
}
}

fillBtn.addEventListener("click", expensiveOperation);

alertBtn.addEventListener("click", () => alert("You clicked me!"));

Nếu bạn click vào button đầu tiên và sau đó click nhanh vào button thứ 2, bạn sẽ thấy không xuất hiện msg cho đến khi các vòng tròn vẽ được render hết. Toán tử đầu tiên đã chặn hành động thứ 2 cho tới khi nó kết thúc quá trình chạy.

Tại sao lại như vậy? Câu trả lời là vì JavaScript, nói chung, là đơn luồng (single threaded). Bạn cần biết khái niệm về luồng.

Threads

Một luồng cơ bản là đơn tiến trình (single process) cái mà là 1 chương trình có thể sử dụng để hoàn thành các nhiệm vụ. Mỗi luồng có thể chỉ làm 1 task trong 1 thời điểm.

A thread is basically a single process that a program can use to complete tasks. Each thread can only do a single task at once

1
Task A --> Task B --> Task C

Mỗi task sẽ chạy tuần tự, 1 task phải hoàn thành trước khi 1 task tiếp theo có thể bắt đầu

Như chúng ta đã nói trước đó, máy tính hiện đại hỗ trợ đã xử lý, vì vậy chúng ta có thể làm nhiều thứ 1 lúc. Ngôn ngữ lập trình có thể hỗ trợ đa luồng có thể sử dụng nhiều cores để hoàn thành nhiều tasks đồng thời.

1
2
Thread 1: Task A --> Task B
Thread 2: Task C --> Task D

JavaScript is single threaded

JavaScript thuần đơn luồng. Thậm chí với nhiều cores, bạn chỉ có thể chạy các tasks trên 1 luồng đơn, được gọi là main thread. Ví dụ của chúng ta sẽ nhìn như sau:

1
Main thread: Render circles to canvas --> Display alert()

Sau một thời gian, JavaScript đã có một số công cụ giúp bạn giải quyết những vấn đề như vậy. Web workers cho phép bạn gửi 1 vài tiến trình JavaScript vào các luồng riêng biệt, gọi là worker để bạn có thể chạy nhiều đoạn mã JavaScript cùng 1 lúc. Bạn thường sử dụng worker để chạy các tiến trình tốn nhiều chi phí ra khỏi luồng chính để tương tác người dùng không bị chặn:

1
2
  Main thread: Task A --> Task C
Worker thread: Expensive task B

Asynchronous code

Web worker rất hữu ích bởi vì nó cũng có những hạn chế. Một trong những hạn chế chính là nó không thể truy cập DOM - bạn không thể có 1 worker làm việc trực tiếp cái gì đó cập nhật UI. Chúng ta không thể render 1 triệu chấm tròn xanh trong worker.

Vấn đề thứ 2 là mặc dù chạy code trong 1 worker là không chặn dừng, nhưng về cơ bản nó vẫn là đồng bộ. Điều này trở thành 1 vấn đề khi 1 hàm dựa vào kết quả của nhiều hàm xử lý trước. Xem xét biểu đồ luồng dưới đây:

1
Main thread: Task A --> Task B

Trong trường hợp này, giả sử Task A làm 1 cái gì đó như lấy 1 ảnh từ server là Task B sau đó làm gì đó như lọc các ảnh. Nếu bạn bắt đầu Task A đang chạy và sau đó ngay lập tức chạy Task B, bạn sẽ gặp lỗi vì ảnh chưa khả dụng.

1
2
  Main thread: Task A --> Task B --> |Task D|
Worker thread: Task C -----------> | |

Trong trường hợp này, Task D sẽ sử dụng kết quả của cả Task B và Task C. Nếu bạn có thể . Nếu chúng ta có thể đảm bảo rằng cả hai kết quả này sẽ có sẵn cùng một lúc, thì OK, nhưng điều này là không thể. Nếu task D cố gắng chạy khi 1 trong 2 đầu vào chưa có, bạn lại nhận được lỗi @@

Để fix các vấn đề như vậy, trình duyệt cho phép chúng ta chạy một hoạt động bất đồng bộ. Chức năng này như Promise cho phép bạn thiết lập một hoạt động đang chạy (ví dụ như lấy ảnh từ server) và sau đó chờ cho đến khi kết quả đã được trả về trước khi chạy một toán tử khác

1
2
Main thread: Task A                   Task B
Promise: |__async operation__|

Vì hoạt động đang diễn ra ở một nơi khác, luồng chính không bị chặn trong khi thao tác bất đồng bộ đang được xử lý.

Tổng kết

  • JavaScript là ngôn ngữ đơn luồng, tức là một thời điểm nó chỉ thực hiện một task. Các kỹ thuật cải thiện nó là Web Worker gửi một số tiến trình vào các luồng riêng biệt. Điểm yếu của phương pháp này là không tương tác được với DOM và cơ bản mỗi Worker vẫn là code đồng bộ
  • Một kỹ thuật khác có lẽ sử dụng nhều hơn là lập trình bất đồng bộ qua Promise. Promise cho phép thực hiện các hành động khác trong quá trình chờ đợi 1 hoạt động nào đó tốn kém (ví dụ chờ ảnh). Theo góc nhìn cá nhân, đây hoàn toàn là đơn luồng vì chỉ làm 1 task ở 1 thời điểm, ở đây là trong lúc chờ đợi thì luồng chính vẫn chạy chứ không phải vừa download ảnh và vừa xử lý luồng chính thì sẽ là đa luồng.

Tài liệu tham khảo

General asynchronous programming concepts

Author

Ming

Posted on

2020-01-24

Updated on

2021-04-10

Licensed under

Comments