Sức mạnh với đa luồng

Ngày nay với sức mạnh gia tăng không ngừng của máy tính thì những lập trình viên của chúng ta lại càng đau đầu hơn khi cứ phải tận dụng sức mạnh to lớn này để nâng cao hiệu suất của chương trình. Nhưng cũng chính vì thế mà sức mạnh của lập trình đa luồng mới được tận dụng triệt để. Và khi kết hợp với Reactive Programming - Lập trình phản ứng (chịu thật không nghĩ ra từ tiếng Việt nào hay hơn, :D) nó sẽ trở thành cặp đôi hoàn hảo để giúp hiệu suất chương trình của chúng ta tăng lên đáng kể.

Nguyên nhân ra đời

Hãy giả sử chúng ta đang xây dựng một ứng dụng hẹn hò, và trang chủ của chúng ta sẽ có 3 nội dung:

  1. Tin tức
  2. Sự kiện
  3. Danh sách thành viên mới

Để hiện thị được đầy đủ trang chủ chúng ta sẽ cần lấy 3 loại dữ liệu:

  1. Danh sách các tin tức nổi bật
  2. Danh sách các sự kiện phù hợp nhất với người xem
  3. Danh sách các thành viên mới được phép công khai

Cách đơn giản nhất là chúng ta sẽ viết thế này:

 HomeData homeData = new HomeData();
 homeData.setNewsList(getHotNewsList());
 homeData.setEventList(getMatchingEvents());
 homeData.setMemberList(getNewPublicMembers());

Trông cũng rất rõ ràng và sạch đẹp đúng không? Nhưng có một điểm ở đây đó là 3 dữ liệu tin tức, sự kiện, và thành viên mới không có sự liên quan nào với nhau và chúng đều được lưu ở cơ sở dữ liệu. Có nghĩa là việc của web server chỉ là gọi để lấy dữ liệu từ database thôi, còn những logic nặng nề thì database đã lo hết rồi. Vậy thì cần gì phải gọi tuần tự nhỉ? Cứ gọi song song rồi tổng hợp lại dữ liệu thôi. Đó chính là tư tưởng để Reactive ra đời.

Định nghĩa

Đọc wiki thì thôi rồi, khỏi hiểu luôn, nên mình cũng đồng tình với 1 bài viết trên redhat. Lập trình phản ứng (Reactive Programming) là lập trình bất đồng bộ với các luồng dữ liệu. Có nghĩa là chúng ta sẽ tận dụng cơ chế đa luồng để lấy dữ liệu từ nhiều nguồn khác nhau, các luồng dữ liệu này là độc lập, không liên quan gì đến nhau nhưng chúng ta có thể kết hợp chúng lại khi cần. Nếu định nghĩa này vẫn còn phức tạp, bạn có thể hiểu đơn giản nó là sự kết hợp giữa lập trình đa luồng và Observer design pattern.

Giải quyết bài toán

Quay trở lại bài toán web hẹn hò, bây giờ chúng ta sẽ sử dụng 3 luồng độc lập để lấy 3 loại thông tin khác nhau như sau.

Và source code sẽ kiểu thế này:

return Reactive.single()
    .register("newsList", this::getHotNewsList)
    .register("events", this::getMatchingEvents)
    .register("members", this::getNewPublicMembers)
    .blockingGet(it ->
        HomeData.builder()
            .newsList(it.get("newsList"))
            .eventList(it.get("events"))
            .memberList(it.get("members"))
            .build()
    );

Cũng có dài dòng hơn một chút nhưng vẫn khá sạch và đẹp, :). Ví dụ đầy đủ các bạn có thể tham khảo ở đây.

So sánh hiệu năng

Giờ mới đến phần thú vị nhất. Quảng cáo hay ho như vậy nhưng rốt cuộc là reactive sẽ giúp chúng ta cải thiện được hiêu năng bao nhiêu? Chúng ta sẽ test 100.000 lần gọi cho 2 loại code kiểu này:


long sequenceCallElapsedTime = Performance.create()
    .loop(100000)
    .test(sequenceHomeController::getHomeData)
    .getTime();
long rxCallElapsedTime = Performance.create()
    .loop(100000)
    .test(rxHomeController::getHomeData)
    .getTime();
System.out.printf(
    "sequenceCallElapsedTime: %d\nrxCallElapsedTime: %d\n",
    sequenceCallElapsedTime,
    rxCallElapsedTime);

Và bạn biết không, kết quả thật sự ấn tượng:

sequence call elapsed time: 18423
reactive call elapsed time: 9853

Điều đó có nghĩa là lập trình đa luồng với reactive giúp chúng ta tăng được gấp đôi tốc độ so với gọi tuần tự truyền thống cho bài toán lấy dự liệu web hẹn hò này.

Một số lưu ý

  1. Lập trình reactive chỉ thực sự có tác dụng khi các luồng dữ liệu là độc lập với nhau, đối với các bài toàn mà dữ liệu phụ thuộc vào nhau hãy cân nhắc sử dụng lập trình theo cách tuần tự truyền thống
  2. Lập trình với đa luồng lúc nào cũng rất khó debug vì không biết khi nào hàm được gọi, vậy bắt buộc phải sử dụng log để theo dõi quá trình gọi hàm và từ đó có cơ sở debug khi có lỗi xảy ra.
  3. Lập trình reactive với web sẽ buộc phải sử dụng blocking get, vì người dùng web, hoặc người dùng gọi HTTP sẽ luôn luôn chờ kết quả trả về. Vậy hãy cân nhắc một thời gian timeout hợp lý.
  4. Việc gọi rất nhiều lệnh đồng thời cũng đồng nghĩa với việc có thể nhận rất nhiều lỗi đồng thời được trả về, vậy hãy cố gắng xử lý lỗi thay vì throw exception để có thể trả được về dữ liệu hợp lý cho người dùng
  5. Code của bạn sẽ bị dài ra đáng kế khi lúc nào cũng phải cài đặt hàm callback, nhưng thật may mắn là chúng ta có lambda expression nên cũng đỡ.
  6. Hãy cố gắng thành thạo lập trình đa luồng để kết hợp được tốt với Reactive programming
  7. Đừng lạm dụng Reactive programming vì nó cũng chỉ cố một số lượng thread nhất định, nếu hết thread thì các task sẽ bị cho và queue và hiệu năng cũng sẽ không được đảm bảo.

Tổng kết

Reactive thực sự hay ho và sự thật là nó có thể tận dụng được tối đa sức mạnh của máy tính thông qua cơ chế đa luồng, giúp chúng ta nâng cao được hiệu suất của chương trình lên đáng kể, vậy đừng chần chừ đề đưa Reactive vào dự án của bạn nhé.

Tham khảo

  1. Wiki
  2. 5 Things to Know About Reactive Programming
  3. Ví dụ