Asynchronous Programming - #1 Tổng quan

Mô hình Task Asynchronous Programming (TAP) cung cấp một sự trừu tượng về code bất đồng bộ. Bạn viết code từng dòng từng dòng như mọi khi. Bạn có thể đọc code kiểu như là câu nào viết trước thì hoàn thành trước, một cách tuần tự. Trình biên dịch thực hiện nhiều phép biến đổi bởi vì một vài câu lệnh có thể bắt đầu chạy và trả về một Task đại diện cho công việc đang diễn ra.

Đó là mục tiêu của cú pháp này: làm cho code dễ đọc như bình thường, nhưng khi thực thi thì phức tạp hơn nhiều dựa trên sự phân bố tài nguyên và khi tác vụ hoàn thành. Nó tương tự như cách mọi người đưa ra hướng dẫn cho những tiến trình mà bao gồm tác vụ bất đồng bộ. Thông qua bài viết này, sẽ có một ví dụ về hướng dẫn làm bữa sáng và thấy được cách async và await để dễ suy luận về code hơn, nó bao gồm chuỗi hướng dẫn bất đồng bộ. Bạn viết hướng dẫn giống như danh sách giải thích làm bữa sáng như thể nào dưới đây :

  1. Rót một tách cà phê
  2. Làm nóng chảo, chiên hai cái trứng
  3. Rán 3 miếng thịt xông khói
  4. Nướng hai miếng bánh mì
  5. Thêm bơ và chà bông vô bánh mì
  6. Rót một ly nước cam

Nếu bạn có kinh nghiệm nấu nướng, bạn sẽ làm những cái ở trên một cách bất đồng bộ. Bạn bắt đầu bằng việc làm nóng chảo rồi bắt đầu rán thịt xông khói. Bạn đặt bánh mì vô lò nướng, sau đó chiên trứng. Mỗi bước của tiến trình, bạn bắt đầu một tác vụ, sau đó bạn tập trung vô những tác vụ đang cần thiết.

Nấu bữa sáng là một ví dụ hay của việc bất đồng bộ không phải song song. Một người (hoặc luồng) có thể xử lý tất cả những tác vụ này. Tiếp tục tương tự bữa sáng, một người có thể làm bữa sáng bất đồng bộ bằng cách bắt đầu một tác vụ trước khi tác vụ trước đó kết thúc. Quy trình nấu ăn vẫn diễn ra dù có ai đang xem hay không. Ngay sau khi bạn bắt đầu làm nóng chảo thì bạn có thể bắt đầu nướng thịt xông khói. Khi thịt xong khói được rán thì bạn có thể đi bỏ miếng bánh mình vô lò nướng.

Đối với một thuật toán song song, bạn cần có nhiều đầu bếp (hoặc luồng). Một người làm trứng, người làm thịt xông khói, dạng vậy. Mỗi người sẽ chỉ tập trung vào một tác vụ. Mỗi người ( hoặc luồng) có thể bị chặn đồng bộ để chòn thịt xông khói xông để lật hoặc bánh mì chín.

Dưới đây là những hướng dẫn làm bữa ăn sáng được viết lại dưới dạng code:



Chuẩn bị bữa ăn sáng không đồng bộ tốn khoảng 30 phút bởi vì nó tính dựa trên tổng thời gian của mỗi tác vụ.

Máy tính nó không như con người có thể giải thích hay nói chuyện được. Máy tính nó sẽ chặn không cho câu lệnh kế tiếp chạy cho đến khi nó hoàn thành công việc. Điều đó sẽ tạo ra một bữa ăn không chút ngon miệng gì cả. Tác vụ càng về sau không thể bắt đầu cho đến khi tác vụ trước hoàn thành. Nó sẽ tốn nhiều thời gian hơn để tạo bữa sáng, và đôi khi món ăn bị nguội trước khi được ăn nữa.

Nếu bạn muốn máy tính thực thi những hướng dẫn trên một cách bất đồng bộ, bạn phải viết code bất đồng bộ.

Một số cái cần phải bận tâm khi viết chương trình ngày nay. Giả sử bạn viết chương trình phía người dùng, bạn muốn UI phản hồi trước khi người dùng nhập gì đó vào. Ứng dụng của bạn không nên để cuộc gọi bị treo khi mà nó đang tải dữ liệu từ trang web. Khi bạn viết chương trình phía server, bạn không muốn mấy cái luồng bị chặn lại. Những luồng này có thể phục vụ cho những request khác. Sử dụng code không đồng bộ khi code bất đồng bộ có thể làm giảm khả năng mở rộng quy mô để ít tốn kém hơn. Bạn sẽ phải trả giá cho những luồng bị chặn.

Các ứng dụng hiện đại thành công cần có code bất đồng bộ. Nếu không có ngôn ngữ hỗ trợ, viết code bất đồng bộ cần phải có callbacks, event hoàn thành hoặc các cách khác để ẩn đi mục đích ban đầu của code. Lợi ích của code không đồng bộ là nó từng hành động một rất dễ để rà soát và hiểu. Mô hình bất đồng bộ truyền thống bắt bạn phải tập trung lên code bất đồng bộ, chứ không phải là hành động cơ bản trong code.

Đừng chặn mà hãy chờ 

Đoạn code lúc nãy là thể hiện của 1 bad practice: viết code đồng bộ để thực hiện hành động bất đồng bộ. Như đã viết, code nó chặn luồng thực thi nó khỏi những công việc khác. Nó sẽ không bị gián đoạn khi có những tác vụ khác trên tiến trình. Nó giống như là khi bạn cứ chằm chằm vào cái lò nướng để đợi bánh mì nhảy lên mặc kệ mọi người nói thế nào.

Nào bắt đầu bằng việc cập nhật code để luồng không bị chặn khi tác vụ đang chạy. Sử dụng từ khóa await cung cấp một cách không cần phải chăn lại để bắt đầu tác vụ, sau đó tiếp tục thực thi khi mà tác vụ đó hoàn thành. Một phiên bản đơn giản của việc làm bữa sáng bằng code.


💡 Tổng thời gian thực thi cũng gần như giống với phiên bản đồng bộ. Do đoạn code này chưa vận dụng  hết lợi ích của những tính năng trong lạp trình bất đồng bộ.

💡 Nội dung phương của của FryEggsAsync, FryBaconAsync, ToastBreadAsync đã được update trả về kiểu Task<Egg>, Task<Bacon>, và Task<Toast> tương ứng. Phương thức được thay đổi tên bằng cách thêm hậu tố "Async" vô nữa. Toàn bộ code sẽ được đưa ở cuối bài viết này nhé.

Đoạn code trên không chặn những quả trứng hay thịt xông khói đang nấu. Đoạn code trên sẽ không bắt đầu bất kỳ tác vụ nào khác. Bạn đặt bánh mì vào lò nước và nhìn nó cho tới khi nó nhảy lên. Nhưng ít nhất, bạn có phản hồi lại ai đang muốn sự chú ý của bạn. Trong một nhà hàng, nơi có nhiều order, đầu bếp có thể bắt đầu nấu trong khi order trước đó vẫn đang trong giai đoạn làm.

Bây giờ, luồng hoạt động trong bữa sáng không bị chặn trong khi đợi bất kỳ tác vụ mà chưa hoàn thành. Với một vài ứng dụng, thay đổi này là điều cần thiết. Một ứng dụng GUI vẫn phản hồi người dùng chỉ với thay đổi này. Tuy nhiên, trong ngữ cảnh này, bạn muốn nhiều hơn. Bạn không muốn mỗi tác vụ thực thi tuần tự. Tốt hơn là bắt đầu một tác vụ trước khi tác vụ trước đó hoàn thành.

Bắt đầu tác vụ đồng thời

Trong nhiều ngữ cảnh, bạn muốn bắt đầu với một vài tác vụ độc lập ngay lập tức. Sau đó, mỗi tác vụ hoàn thành xong, bạn có thể tiếp tục những việc khác đang chờ. Tương tự trong bữa sáng lúc nãy, bạn có thể làm bữa sáng nhanh hơn. Bạn cũng có thể làm mọi chuyện xong đúng lúc và có đồ ăn ngon không bị nguội.

System.Threading.Tasks.Task và những loại liên quan là lớp bạn có thể sử dụng để lập luận về tác vụ đang được thực hiện. Điều đó cho phép bạn viết code tương tự hành động thực tế khi làm bữa sáng. Bạn bắt đầu làm trứng, rồi thịt xông khói cùng lúc. Mỗi cái cần có hành động, bạn chú ý vô tác vụ đó, và kiểm tra hành động tiếp theo, sau đó chờ những cái bạn cần chờ.

Bạn bắt đầu một tác vụ và làm tác vụ đó ở đối tượng Task - đại diện cho một việc làm. Bạn sẽ await (đợi) mỗi tác vụ xong trước khi nó có kết quả.

Hãy thực hiện những thay đổi vô trong code. Đầu tiên là giữ lại giá trị của mấy cái tác vụ cho mấy hành thay vì chờ chúng:

Kế tiếp, bạn có thể di chuyển những có lệnh có await cho bacon và egg ở cuối phương thức trước khi được phục vụ:


Bất đồng bộ trên tốn khoảng 20 phút, tiết kiệm được xíu thời gian do nó chạy đồng thời.

Code chạy tốt hơn. Bạn bắt đầu chạy bất đồng bộ tất cả một lần. Bạn chờ mỗi tác vụ khi mà bạn cần kết quả. Code ở trên có thể quen thuộc với code trên ứng dụng web, nó tạo nhiều request của nhiều microservice khác nhau, sau đó kết hợp lại thành kết quả trả về single page. Bạn sẽ tạo ra những request ngay lập tức, sau đó chờ tất cả tác vụ và trả về trang web.

Các thành phần đi cùng tới tác vụ (task)

Bạn đã chuẩn bị tất cả cho bữa sáng cùng một lúc ngoại trừ bánh mì nướng. Làm bánh mì nướng là một thành phần của quy trình bất đồng bộ (nướng bánh mì), và quy trình đồng bộ (thêm bơ và chà bông). Chỉnh sửa đoạn code minh họa dưới đây cho phần quan trọng này:

🛈 Quan trọng: Thành phần của hoạt động bất đồng bộ có hoạt động kế tiếp là hoạt động đồng bộ thì tất cả chúng là hoạt động bất đồng bộ. Nói cách khác, nếu có bất kỳ thành phần của hoạt động là bất đồng bộ, thì toàn bộ hoạt động đó gọi là bất đồng bộ.

Đoạn code ở trên cho thấy là bạn có thể sử dụng đối tượng Task or Task<TResult> để giữ tác vụ đang chạy. Bạn await mỗi task trước khi nó trả về kết quả. Bước tiếp theo bạn tạo một phương thức để kết hợp các công việc trong đó. Trước phục vụ bữa sáng, bạn muốn chờ tác vụ đại diện cho nướng bánh mì trước khi thêm bơ và chà bông. Bạn có thể gôm chúng lại như đoạn code dưới đây:


Đoạn code trên có dùng ký hiệu async. Đây là một dấu hiệu cho trình biên dịch biết là có một lệnh awaittrong phương thức; nó có thực hiện bất đồng bộ. Phương thức đại diện cho tác vụ nướng bánh mì, sau đó thêm bơ và chà bông. Phương thức trả về một Task<TResult> đại diện thành phần của ba hoạt động. Ở ngoài đoạn code chính nó sẽ thành:

Đoạn code thay đổi vừa rồi minh họa một kỹ thuật quan trọng khi viết code bất đồng bộ. Bạn viết tác vụ bằng cách chia hoạt động nó thành một phương thức mới trả về một tác vụ (task). Bạn có thể chọn lúc await tác vụ đó. Bạn có thể bắt đầu những tác vụ đồng thời.

Những ngoại lệ bất đồng bộ

Tới đây, bạn giả sử những tác vụ đã hoàn thành thành công. Phương thức bất đồng bộ quăng những ngoại lệ, giống như những phần đồng bộ của nó. Bất đồng bộ hỗ trợ những ngoại lệ và xử lý lỗi giống như cho code không bất đồng bộ vậy: Bạn nên viết code đọc như không bất đồng bộ. Tác vụ quăng ngoại lệ khi nó không hoàn thành thành công. Code ở client có thể bắt những ngoại lệ này khi một tác vụ bắt đầu đang chờ. Ví dụ, hãy giả sử là lò nướng bánh có lửa khi nướng bánh mì. Bạn có thể mình họa việc đó bằng cách chỉnh sửa phương thức ToastBreadAsync:

Chú ý là có một vài tác vụ hoàn thành ở giữa lúc mà lò nướng bắt lửa và lúc xuất hiện ngoại lệ. Khi một tác vụ chạy bất đồng bộ quăng ngoại lệ, tác vụ đó bị lỗi. Đối tượng Task giữ cái ngoại lệ đó trong thuộc tính Task.Exception . Những tác vụ quăng lỗi đó ra khi nó await.

Có hai kỹ thuật quan trọng cần phải hiểu: một ngoại lệ được lưu trong một tác vụ lỗi như thế nào, và ngoại lệ được giải nén và quăng lại khi await tác vụ lỗi như thế nào.

Khi code chạy bất đồng bộ quăng một ngoại lệ, ngoại lệ đó được trữ trong Task. Thuộc tính Task.Exception là một System.AggregateException bởi vì nhiều hơn một ngoại lệ có thể được quăng khi thực hiện bất đồng bộ. Bất kỳ ngoại lệ được quăng ra sẽ thêm vô collection AggregateException.InnerExceptions Nếu thuộc tính Exception là null, một AggregateException được tạo và ngoại lệ được quăng ra là item đầu tiên trong collection.

Phần lớn tình huống phổ biến khi tác vụ lỗi là thuộc tính Exception chỉ có chính xác một ngoại lệ. Khi code await một tác vụ lỗi, ngoại lệ đầu tiên trong AggregateException.InnerException sẽ được quăng lại. Đó là lý do tại sao output từ ví dụ cho thấy rằng InvalidOperationException thay vì AggregateException. Lấy ngoại lệ đầu tiên làm với phương thức bất đồng bộ giống như làm với code không đồng bộ. Bạn có thể kiểm tra thuộc tính Exception trong code của bạn khi tình huống của bạnn có nhiều ngoại lệ.

Trước khi đi tiếp, comment lại hai dòng trong phương thức ToastBreadAsync dưới đây. Bạn không muốn bắt đầu một ngọn lửa khác:

Chờ tác vụ một cách hiệu quả

Một chuỗi của câu lệnh await ở cuối đoạn code trên có thể cải thiện bằng cách sử dụng một phương thức của lớp Task. Một trong những API là WhenAll, mà nó trả về một Task hoàn thành xong tất cả tác vụ trong tham số nó truyền vào, giống như đoạn code dưới đây nè:

Có một lựa chọn nữa là sử dụng WhenAny, nó trả về Task<Task> khi hoàn thành bất kỳ tác vụ nào trong tham số truyền vào. Bạn có thể chờ cái tác vụ trả về, biết nó đã hoàn thành xong. Code tiếp theo cho thấy cách bạn sử dụng WhenAny để chờ tác vụ đầu tiên xong và tiến hành trả về kết quả. Sau khi trả về kết quả, bạn đem cái tác vụ đã xong đó ra khỏi đống tác vụ truyền vô WhenAny.


Sau tất cả những thay đổi, code cuối cùng sẽ như thế này đây:

Phiên bản chuẩn bị bữa sáng bất đồng bộ này tốn khoảng 15 phút bởi vì một vài tác vụ chạy cùng lúc, và code theo dõi nhiều tác vụ cùng một lúc và chỉ thực hiện khi hành động.

Bản code cuối cùng là bất đồng bộ. Nó phản ánh chính xác một người đầu bếp làm bữa sáng như thế nào. So sánh với những code trước đó trong bài viết này. Những hành động chính vẫn rõ ràng khi đọc code. Bạn có thể đọc code như các bước hướng dẫn làm bữa sáng ở đầu của bài viết này. Tính năng ngôn ngữ asyncawait cung cấp cách dịch mỗi người làm theo các bước hướng dẫn: bắt đầu tác vụ ngay khi bạn có thể và không cần phải chặn luồn chạy chỉ để hoàn thành xong code hiện tại.

Nguồn: Asynchronous Programming with async and await

Nhận xét

Bài đăng phổ biến