Coder Sơn Trang

https://codersontrang.files.wordpress.com/2017/09/codersontrang-com.png

Trong quá trình học và làm việc, chúng ta thường nghe nói về hai thuật ngữ là Blocking và Non-Blocking. Hai thuật ngữ này mô tả cách mà một chương trình thực hiện các lệnh của nó. Nói một cách đơn giản, nếu chương trình được thực hiện theo mô hình Blocking, tức là các lệnh được thực hiện theo tuần tự. Khi một lệnh ở phía trước chưa hoàn thành, các lệnh phía sau sẽ phải đợi cho đến khi lệnh trước hoàn thành. Trong trường hợp các lệnh trước liên quan đến việc xử lý I/O hoặc mạng, nó sẽ làm chậm quá trình thực hiện các lệnh phía sau. Mô hình Blocking tồn tại từ lịch sử, khi máy tính chỉ có thể xử lý một tác vụ tại một thời điểm. Nhưng ngày nay, với sự phát triển của công nghệ, máy tính có thể thực hiện nhiều công việc song song. Vì vậy, mô hình Non-Blocking được tạo ra để tận dụng tối đa tài nguyên xử lý của máy tính và tránh lãng phí. Để thực hiện mô hình Non-Blocking, người ta sử dụng nhiều thread khác nhau hoặc sử dụng các mẫu thiết kế như event-loop.

Trong bài viết này, mình sẽ đưa ra một ví dụ để minh họa cách thức hoạt động của Blocking và Non-Blocking. Ví dụ này mô tả quá trình lấy dữ liệu từ 3 hàm khác nhau và in kết quả lên màn hình. Các hàm lấy dữ liệu trong ví dụ này chỉ đơn giản là mô phỏng một công việc trong một khoảng thời gian nhất định. Trong thực tế, công việc này có thể liên quan đến đọc dữ liệu từ file hoặc cơ sở dữ liệu, hoặc liên quan đến kết nối mạng như gọi webservice. Các hàm lấy dữ liệu theo cơ chế Blocking và Non-Blocking sẽ được so sánh để thấy sự khác nhau giữa hai cách thức này.

Trước hết, hãy xem hình ảnh minh họa về Blocking và Non-Blocking dưới đây:

Phần phía trên miêu tả việc thực hiện theo cơ chế Blocking. Ở đây, các công việc tiếp sau phải chờ công việc phía trước hoàn thành mới có thể bắt đầu. Các bước sẽ được thực hiện như sau:

  1. Gọi hàm dataSync1.get() để lấy dữ liệu. Vì đây là Blocking, các công việc tiếp sau phải chờ.
  2. Gọi hàm printData(d1) để in dữ liệu lấy được từ dataSync1.get(). Đây cũng là Blocking.
  3. Gọi hàm dataSync2.get() để lấy dữ liệu. Mặc dù không liên quan gì đến hai lệnh trước, nhưng nó vẫn phải đợi một thời gian xử lý.
  4. Gọi hàm printData(d2) để in dữ liệu lấy được từ dataSync2.get(). Đây cũng là Blocking.
  5. Gọi hàm dataSync3.get() để lấy dữ liệu. Đây là Blocking.
  6. Gọi hàm printData(d3) để in dữ liệu lấy được từ dataSync3.get(). Đây là Blocking.
Có Thể Bạn Quan Tâm :   Tản văn tiếng anh là gì? 3 Đặc điểm nổi bật của một tản văn

Ở phần này, mọi thao tác đều là Blocking, do đó thời gian để thực hiện toàn bộ các công việc sẽ bằng tổng thời gian của từng công việc.

Phần phía dưới là phần thể hiện việc thực hiện các công việc này với cả Blocking và Non-Blocking. Các công việc in dữ liệu printData(d1), printData(d2), printData(d3) vẫn là Blocking nhưng có sự tham gia của Non-Blocking trong việc lấy dữ liệu dataAsync1.get(), dataAsync2.get(), dataAsync3.get(). Các công việc Non-Blocking sẽ bắt đầu gần như ngay lập tức và không cần phải chờ các công việc phía trước hoàn thành. Sau khi có kết quả, các công việc Non-Blocking sẽ gọi lại callback để in kết quả lên màn hình. Các bước cụ thể như sau:

  1. Gọi hàm dataAsync1.get() để lấy dữ liệu. Vì đây là Non-Blocking, quá trình thực thi sẽ không dừng lại ở đây mà tiếp tục thực hiện các lệnh tiếp sau. Kết quả sau khi lấy được dữ liệu sẽ được in ra bằng callback.
  2. Ngay sau đó, gọi hàm dataAsync2.get() cùng với đăng ký callback. Vì là Non-Blocking, quá trình thực hiện cũng giống như trên.
  3. Tiếp theo, gọi hàm dataAsync3.get() cũng được thực hiện tương tự. Ở đây, ba lệnh gọi để lấy dữ liệu được thực hiện gần như đồng thời mà không cần phải chờ nhau.
  4. Khi hàm dataAsync2.get() đã lấy được dữ liệu và callback được gọi để in kết quả lên màn hình, hàm printData(d2) là Blocking đang thực hiện.
  5. Trong khi hàm printData(d2) đang thực hiện, hàm dataAsync1.get() đã hoàn tất việc lấy dữ liệu và callback của nó được gọi. Tuy nhiên, vì printData(d2) là Blocking đang thực hiện, việc thực hiện printData(d1) sẽ phải chờ.
  6. Tương tự như trên, khi hàm dataAsync3.get() hoàn tất việc lấy dữ liệu, callback của nó được gọi. Lần này, printData(d3) không chỉ phải chờ printData(d2) như trên mà còn phải chờ thêm printData(d1) vì printData(d1) cũng là Blocking. Sau khi cả printData(d2) và printData(d1) hoàn thành, printData(d3) được thực hiện và toàn bộ quá trình hoàn tất.
Có Thể Bạn Quan Tâm :   MStudy

Bây giờ, nhìn lại hình vẽ, ta có thể thấy Non-Blocking rút ngắn thời gian thực hiện chương trình hơn là Blocking. Thời gian rút ngắn này không phải vì các công việc được thực hiện nhanh hơn mà do các công việc được thực hiện cùng một lúc hơn.

Dưới đây là đoạn code demo cho việc thực hiện với Blocking và Non-Blocking bằng Java.

DataSync.java

“`java
public class DataSync {
private int id;
private long simulationDuration;

DataSync(int id, long simulationDuration){
this.id = id;
this.simulationDuration = simulationDuration;
}

public String get(){
try{
Thread.sleep(this.simulationDuration);
} catch (InterruptedException e) {
e.printStackTrace();
}
return “data-” + id;
}
}
“`

Class DataSync đại diện cho một nguồn dữ liệu có thể lấy về theo cơ chế Blocking. Nguồn dữ liệu này có hai thuộc tính là id và simulationDuration.

MainSync.java

“`java
public class MainSync {
public static void main(String[] args) {
long startTime, endTime;

DataSync dataSync1 = new DataSync(1, 5000); //5s
DataSync dataSync2 = new DataSync(2, 3000); //3s
DataSync dataSync3 = new DataSync(3, 6000); //6s

startTime = System.currentTimeMillis();
System.out.println(“Start”);
String d1 = dataSync1.get();
printData(d1);

String d2 = dataSync2.get();
printData(d2);

String d3 = dataSync3.get();
printData(d3);

System.out.println(“Done”);
endTime = System.currentTimeMillis();
System.out.print(“Execution time (ms): ” + (endTime – startTime));
}

private static void printData(String data){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(“Synchronously printing ” + data);
}
}
“`

Class MainSync bao gồm phương thức main() là điểm bắt đầu của chương trình. Trước tiên, chúng ta khởi tạo ba nguồn dữ liệu Blocking là dataSync1, dataSync2 và dataSync3 có thời gian khác nhau là 5 giây, 3 giây và 6 giây. Sau đó, ta gọi phương thức get() của từng nguồn dữ liệu để lấy dữ liệu về. Kết quả sẽ được in ra ngay sau khi lấy được dữ liệu từ các nguồn qua phương thức printData(). Phương thức printData() là Blocking và được giả lập thời gian thực hiện 1 giây. Cuối cùng, ta in thời gian thực hiện chương trình.

Khi chạy chương trình, kết quả sẽ được in ra theo thứ tự đã mô tả ở đầu bài viết và thời gian thực hiện là 17001 mili giây.

DataAsync.java

“`java
import java.util.function.Supplier;

public class DataAsync implements Supplier {
private int id;
private long simulationDuration;

DataAsync(int id, long simulationDuration){
this.id = id;
this.simulationDuration = simulationDuration;
}

@Override
public String get() {
try {
Thread.sleep(simulationDuration);
} catch (Exception e) {
}
return “data-” + id;
}
}
“`

Có Thể Bạn Quan Tâm :   Thư bảo lãnh (Letter of Guarantee) là gì? Nội dung của thư bảo lãnh

Class DataAsync đại diện cho một nguồn dữ liệu có thể lấy về theo cơ chế Non-Blocking. Các thuộc tính của lớp này giống như lớp DataSync bên trên. Lớp này cũng implement interface Supplier để sử dụng trong CompletableFuture.

MainAsync.java

“`java
import java.util.concurrent.*;

public class MainAsync {
public static void main(String[] args) {
long startTime, endTime;

CountDownLatch latch = new CountDownLatch(3);

DataAsync dataAsync1 = new DataAsync(1, 5000);
DataAsync dataAsync2 = new DataAsync(2, 3000);
DataAsync dataAsync3 = new DataAsync(3, 6000);

startTime = System.currentTimeMillis();
System.out.println(“Start”);
try {
CompletableFuture.supplyAsync(dataAsync1).thenAccept(d1 -> {
printData(d1);
latch.countDown();
});

CompletableFuture.supplyAsync(dataAsync2).thenAccept(d2 -> {
printData(d2);
latch.countDown();
});

CompletableFuture.supplyAsync(dataAsync3).thenAccept(d3 -> {
printData(d3);
latch.countDown();
});

latch.await();
System.out.println(“Done”);
endTime = System.currentTimeMillis();
System.out.print(“Execution time (ms): ” + (endTime – startTime));
} catch (Exception e) {
}
}

private static void printData(String data){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(“Synchronously printing ” + data);
}
}
“`

Class MainAsync cũng có phương thức main(). Trong phương thức này, ta sử dụng CompletableFuture của Java 8 để thực hiện việc lấy dữ liệu theo cơ chế Non-Blocking. Ta tạo ba CompletableFuture, mỗi CompletableFuture gọi phương thức supplyAsync() và đăng ký callback bằng phương thức thenAccept(). Các nguồn dữ liệu được truyền vào phương thức supplyAsync(). Đồng thời, ta sử dụng CountDownLatch để chờ tất cả các callback hoàn tất. Sau khi chạy xong tất cả các tác vụ, ta in thời gian thực hiện chương trình.

Khi chạy chương trình, kết quả sẽ được in ra theo thứ tự mô tả ở đầu bài viết và thời gian thực hiện là 7171 mili giây.

Hiện nay, khi phần cứng ngày càng phát triển, các ứng dụng cần tận dụng tối đa tài nguyên để sử dụng một cách hiệu quả. Non-Blocking là mô hình mà các ứng dụng luôn hướng đến. Trong một số ngôn ngữ truyền thống như Java, mỗi lệnh đa phần là Blocking. Developer có thể tạo ra một cơ chế Non-Blocking trong chương trình bằng cách sử dụng các API như CompletableFuture hoặc Reactive Stream (RxJava). Trong các nền tảng hiện đại như NodeJS, hầu hết các lệnh đều là Non-Blocking, giúp developer tận dụng tài nguyên một cách hiệu quả, tránh lãng phí và tránh các vấn đề phức tạp trong việc quản lý các luồng xử lý không đồng bộ.

Hy vọng rằng bài viết này sẽ giúp bạn hiểu được khái niệm và sự khác nhau giữa mô hình Blocking và Non-Blocking.

Chúc may mắn!

Back to top button