Asynchronous Programming - #3 Mô hình lập trình tác vụ bất đồng bộ

 

Bạn có thể tránh tình trạng quá tải và cải thiện độ phản hồi của toàn bộ ứng dụng bằng cách sử dụng lập trình bất đồng bộ. Tuy nhiên, những kỹ thuật truyền thống khi lập trình ứng dụng bất đồng bộ có thể phức tạp, khó viết, debug và bảo trì.

C# 5 giớ thiệu một cách tiếp cận đơn giản hơn, lập trình bất đồng bộ, mà thúc đẩy hỗ trợ bất đồng bộ trong .NET Framework 4.5 và cao hơn nữa, .NET Core và Windows Runtime. Trình biên dịch làm công việc khó mà lập trình viên đã từng làm, và ứng dụng của bạn vẫn giữ được cấu trúc logic giống với mã code không đồng bộ. Kết quả là, bạn sẽ có được tất cả lợi ích của lập trình bất đồng bộ với nỗ lực nhỏ.

Chủ đề này cung cấp một cái nhìn tổng quát sử dụng dụng lập trình bất khi nào và như thế nào và bao gồm những đường dẫn tới những chủ đề liên quan chi tiết hơn và có ví dụ luôn.

Async cải thiện khả năng phản hồi

Bất đồng bộ là cần thiết cho các hoạt động có khả năng bị chặn như là truy cập web. Truy cập nguồn tài nguyên của web là chậm hoặc là bị hoãn. Nếu một hoạt động bị chặn trong tiến trình đồng bộ, cả ứng dụng phải chờ. Nếu dùng bất đông bộ, ứng dụng có thể tiếp tục với công việc khác mà không phụ thuộc vào tài liệu web cho đến khi những luồng có tiềm năng bị chặn hoàn thành.

Bảng dưới đây cho thấy các lĩnh vực mà lập trình bất đồng bộ cải thiện khả năng phản hồi. Các APIs từ .NET và Windows Runtime bao gồm những phương thức hỗ trợ lập trình bất đồng bộ.

Lĩnh vực ứng dụng Các kiểu .NET với những phương thức bất đồng bộ Các kiểu Windows Runtime với những phương thức bất đồng bộ
Truy cập web HttpClient Windows.Web.Http.HttpClientSyndicationClient
Làm việc với files JsonSerializer StreamReader StreamWriter XmlReader XmlWriter StorageFile
Làm việc với hình ảnh MediaCapture BitmapEncoder BitmapDecoder
Lập trình WCF Synchronous and Asynchronous Operations

Bất đồng bộ chứng mình một giá trị đặc biệt cho ứng đụng truy cập luồng UI bởi vì tất cả hoạt động liên quan tới UI thường chia sẻ trong một luồng. Nếu bất kỳ tiến trình nào bị chặn trong một ứng dụng đồng bộ, tất cả sẽ bị chặn. Ứng dụng của bạn dừng phản hồi, và bạn có thể kết luận là nó thất bại thay vì chỉ chờ.

Khi bạn sử dụng các phương thức bất đồng bộ, ứng dụng tiếp tục để phản hồi đến UI. Bạn có thể thay đổi hoặc thu nhỏ màn hình, ví dụ, hoặc bạn có thể đóng ứng dụng nếu bạn không muốn chờ nó chạy xong.

Cách tiếp cận dựa trên bất đồng bộ thêm một hộp số tự động tương đương vào danh sách lựa chọn mà bạn có thể chọn từ hoạt động thiết kế bất đồng bộ. Nghĩa là bạn có tất cả lợi ích của lập trình bất đồng bộ truyền thống những tốn ít nỗ lực từ lập trình viên.

Phương thức Async dễ viết

Từ khóa asyncawait trong C# là mấu chốt của lập trình bất đồng bộ. Sử dụng hai từ khóa đó thôi là bạn có thể sử dụng resource trong .NET Framework, .NET Core và Windows Runtime để tạo một phương thức bất đồng bộ dễ như cách viết một phương thức đồng bộ vậy. Những phương thức bất đồng bộ bạn có thể định nghĩ bằng cách sử dụng từ khóa async để gọi phương thức bất đồng bộ.

Dưới đây là ví dụ về phương thức bất đồng bộ. Phần lớn code sẽ giống như bình thường vậy á.

Bạn có thể tìm thấy một ví dụ Windows Presentation Foundation (WPF) hoàn chỉnh có thể tài về từ Lập trình bất đồng bộ với async và await trong C#

Bạn có thể học một vài practice từ ví dụ trên. Bắt đầu với dấu hiệu nhận biết phương thức. Nó có modifier async . Kiểu trả về là Task<int> (Xem thêm mục Reture Types để có thêm sự lựa chọn). Tên phương thức kết thúc bằng từ Async. Trong body của phương thức, GetStringAsynctrả về một Task<string>. Nghĩa là khi bạn await tasksẽ lấy được một string (contents). Trước khi chờ task, bạn có thể làm việc khác mà nó không xài cái string từ GetStringAsync.

Hãy tập trung vào operator await . Nó hoãn GetUrlContentLengthAsync:

  • GetUrlContentLengthAsynckhông thể tiếp tục cho tới khi getStringTaskhoàn thành
  • Trong lúc đó, control trả về gọi GetUrlContentLengthAsync
  • Control tiếp tục ở đây khi getStringTaskhoàn thành
  • await operator sau đó nhận được kết quả string từ getStringTask

Câu lệnh trả về xác định kết quả số nguyên. Bất kỳ phương thức đang chờ GetUrlContentLengthAsync đạt được giá trị độ dài.

Nếu GetUrlContentLengthAsync không có bất kỳ công việc nào nó có thể làm giữa GetStringAsync và chờ cho tới khi nó hoàn thành, bạn có thể đơn giản hóa code của bạn bằng cách gọi hoặc chờ những bằng câu lệnh dưới đây.

Tổng kết những đặc điểm của phương thức bất đồng bộ từ ví dụ trên:

  • Phương thức ký hiệu có modifier là async

  • Tên của phương thức bất đồng bộ, theo quy ước, kết thúc với hậu tố Async

  • Kiểu trả về là một trong những loại sau:

    • Task<TResult> nếu phương thức của bạn có một giá trị trả về và nó type là TResult.
    • Task nếu phương thức của bạn không có giá trị trả về hoặc trả về câu lệnh không có toán hạng
    • voidnếu bạn đang viết một async event handler
    • Bất kỳ kiểu khác có một phương thức GetAwaiter (bắt đầu từ C# 7.0)

    Để có nhiều thông tin, hãy xem mục Return types and parameters

  • Phương thức thường bao gồm ít nhất một biểu thức await , mà nó đánh dấu một điểm để phương thức không thể tiếp tục cho tới khi hoạt động bất đồng bộ hoàn thành. Trong lúc đó, phương thức hoãn lại, và control trả về cho caller của phương thức. Phần kế tiếp của chủ đề này sẽ minh họa việc gì xảy ra ở lúc hoãn lại đó.

Trong phương thức bất đồng bộ, bạn sử dụng từ khóa và kiểu được cung cấp để chỉ ra những cái bạn muốn làm, và trình biên dịch làm phần còn lại, bao gồm việc theo dõi cái gì xảy ra khi control trở lại ở lúc chờ trong phương thức đang bị hoãn. Một số quy trình thông thường, chẳng hạn như vòng lặp và xử lý ngoại lệ, có thể khó để xử lý với code bất đồng bộ truyền thống. Trong một phương thức bất đồng bộ, bạn viết những thành phần nhiều như bạn đang trong lúc viết code đồng bộ, và vấn đề được giải quyết.

Để có thêm nhiều thông xin về bất đồng bộ của những phiên bản của .NET Framework, xem TPL and traditional .NET Framwork asynchronous programming.

Chuyện gì xảy ra trong một phương thức bất đồng bộ

Điều quan trong nhất cần phải hiểu trong lập trình bất đồng bộ là cách các luồng control di chuyển từ phương thức tới phương thức như thế nào. Sơ đồ dưới đây sẽ dẫn bạn đi qua quá trình đó:

Những con số trong sơ đồ tương ứng với thứ tự các bước, bắt đầu khi một phương thức gọi một phương thức bất đồng bộ.

  1. Một phương thức gọi và chờ phương thức bất đồng bộ GetUrlContentLengthAsync

  2. GetUrlContentLengthAsync tạo một thực thể HttpClient và gọi phương thức bất đồng bộ GetStringAsync để tải nội dung của website dưới dạng chuỗi

  3. Có gì đó xảy ra trong GetStringAsync hoãn lại quá trình của nó. Có thể nó phải chờ website để tải hoặc một hoạt động chặn gì đó. Để tránh chặn nguồn tài nguyên, GetStringAsync trả về lại cho cái gọi nó là GetUrlContentLengthAsync

    GetStringAsync trả về một Task<TResult>, TResult là một chuỗi, và GetUrlContentLengthAsync gán task vô biến getStringTask. Task đại diện cho quá trình đang chạy khi gọi GetStringAsync, với một cam kết là tạo ra giá trị thực sự khi nó hoàn thành

  4. Bởi vì getStringTask chưa có await, GetUrlContentLengthAsync có thể làm công việc khác mà công việc đó không cần kết quả cuối cùng từ GetStringAsync. Công việc đó được biểu diễn bằng cách gọi một phương thưsc đồng bộ DoIndepentWork.

  5. DoIndependentWork là một phương thức đồng bộ mà nó làm công việc của nó và trả kết quả về cho chỗ gọi đó.

  6. GetUrlContentLengthAsync đã làm xong việc nó có thể làm mà không cần kết quả từ getStringTask. GetUrlContentLengthAsynckế tiếp muốn tính toán và trả về kết quả độ dài của string vừa tải về được, nhưng phương thức không thể tính toán giá trị cho đến khi phương thức có chuỗi

    Vì thế, GetUrlContentLengthAsync sử dụng một toán tử await để hoãn quá trình của nó và mang control tới phương thức mà nó gọi GetUrlContentLengthAsync. GetUrlContentLengthAsync trả về một Task<int> cho chỗ gọi nó. Task đại diện một lời hứa để tạo ra kết quả số nguyên là độ dài của chuỗi được tải về.

    Lưu ý Nếu GetStringAsync (và do đó getStringTask) hoàn thành trước khi GetUrlContentLengthAsync chờ nó, control vẫn nằm trong GetUrlContentLengthAsync. Chi phí tạm dừng và sau đó trả về cho GetUrlContentLengthAsync sẽ bị lãng phí nếu quá trình bất đồng bộ getStringTask đã hoàn thành và GetUrlContentLengthAsync không phải chờ đến kết quả cuối cùng.

    Trong phương thức gọi, những chỗ khác vẫn tiếp tục. Nó sẽ làm tiếp tục công việc khác mà công việc đó không phụ thuộc vào kết quả từ GetUrlContentLengthAsync trước khi chờ kết quả, hoặc caller có thể chờ ngay lập tức. Phương thức gọi đang chờ GetUrlContentLengthAsync, và GetUrlContentLengthAsync đang chờ GetStringAsync.

  7. GetStringAsync hoàn thành và tạo ra một kết quả chuỗi. Kết quả chuỗi không được trả về bởi việc gọi GetStringAsync theo cách bạn mong đợi. (Hãy nhớ phương thức đã trả về ở bước thứ 3) Thay vào đó, kết quả chuỗi được trữ lại trong task thể hiện sự hoàn thành của phương thức, getStringTask. Toán tử await lấy kết quả từ getStringTask. Câu lệnh gán kết quả đạt được vào contents

  8. Khi GetUrlContentLengthAsync có kết quả chuỗi, phương thức có thể tính toán độ dài của chuỗi. Sau đó công việc của GetUrlContentLengthAsync cũng hoàn thành, và trình xử lý sự kiện đang chờ để có thể tiếp tục. Trong ví dụ đầy đủ ở cuối chủ đề, bạn có thể xác nhận rằng trình xử lý sự kiện lấy và in giá trị độ dài. Nếu chưa quen với lập trình bất đồng bộ, hãy dành vài phút để xem xét sự khác nhau về hành vi giữa đồng bộ và bất đồng bộ. Phương thức đồng bộ trả về khi nó hoàn toàn hoàn thành (bước 5), nhưng phương thức bất đồng bộ trả về một giá trị task khi nó bị hoãn (bước 3 và 6). Khi phương thức bất đồng bộ cuối cùng hoàn thành việc của nó, task được đánh dấu là hoàn thành và có kết quả, nếu có, được lưu trữ trong task.

Các phương thức API có async

Bạn có thể tự hỏi chỗ nào để tìm kiếm các phương thức như GetStringAsync mà hỗ trợ lập trình bất đồng bộ. NETFramework 4.5 hoặc cao hơn và .NET Core có những thành viên mà nó làm việc với async và await. Bạn có thể nhận thấy chúng bằng hậu tố Async được thêm cho mỗi thành viên, và bằng kiểu trả về của Task hoặc Task<TResult>. Ví dụ, lớp System.IO.Stream bao gồm các phương thức như CopyToAsync, ReadAsyncWriteAsync tương ứng với phương thức đồng bộ CopyTo, ReadWrite.

Windows Runtime cũng có nhiều phương thức mà bạn có thể sử dụng async và await trong các ứng dụng Windows. Để biết thêm thông tin, hãy xem Threading and async programming cho lập trình UWP và Asynchronous programming (Windows Store apps)Quickstart: Calling asynchronous APIs trong C# hoặc Visual Basic nếu bạn sử dụng những phiên bản mới nhất của Windows Runtime.

Threads (Luồng)

Phương thức bất đồng bộ có mục đích không chặn các hoạt động. Một biểu thức await trong phương thức bất đồng bộ không chặn luồng hiện tại trong khi task cần chờ vẫn đang chạy. Thay vào đó, biểu thức đăng ký phần còn lại của phương thức như một phần tiếp tục và trả quyền điều khiển cho chỗ gọi phương thức bất đồng bộ.

Từ khóa async và await không tạo ra những luồng mới. Phương thức bất đồng bộ không yêu cầu đa luồng bởi vì một phương thức bất đồng bộ không chạy trên luồng riêng của nó. Phương thức chạy trên một ngữ cảnh đồng bộ và sử dụng thời gian trên luồng chỉ khi phương thức hoạt động. Bạn có thể sử dụng Task.Run để di chuyển công việc liên quan tới CPU-bound để chạy luồng background, nhưng một luồng background không giúp được gì cho quy trình nó chỉ chờ kết quả có sẵn.

Cách tiếp cận bằng async đối với lập trình bất đồng bộ là nó được ưu tiên hơn những cách tiếp cận đã tồn tại trong hầu hết các trường hợp. Đặc biệt, cách tiếp cận này tốt hơn lớp BackgroundWorker cho hoạt động liên quan tới I/O bởi vì code đơn giản hơn và bạn không cần đề phòng các điều kiện khó khăn. Trong sự kết hợp với phương thức Task.Run, phương thức bất đồng bộ là tốt hơn BackgroundWorker đối với CPU-bound bởi vì lập trình bất đồng bộ tách chi tiết điều phối của code đang chạy của bạn từ công việc mà Task.Run chuyển đến nhóm luồng.

async và await

Nếu bạn xác định rằng một phương thức là một phương thức bất đồng bộ bằng việc sử dụng modifier async, bạn có hai khả năng sau:

  • Phương thức đánh dấu bất đồng bộ có thể sử dụng await để chỉ định cái điểm dừng lại để chờ. Toán tử await nói cho trình biên dịch là phương thức bất đồng bộ không thể tiếp tục qua thời điểm đó cho tới khi quá trình chờ bất đồng bộ hoàn thành. Trong khi chờ đợi, quyền điều khiển trả về trình gọi của phương thức bất đồng bộ.

    Việc tạm dừng một phương thức bất đồng bộ ở một biểu thức await không tạo thành một lối thoát cho phương thức, và những khối cuối cùng không chạy

  • Phương thức đánh dấu bất đồng bộ có thể được chờ bởi phương thức gọi nó

Một phương thức bất đồng bộ điển hình bao gồm một hoặc nhiều lần xuất hiện của toán tử await, nhưng sự vắng mặt của biểu thức await không gây ra lỗi. Nếu một phương thức bất đồng bộ không sử dụng một toán tử để đánh dấu điểm chờ, phương thức thực thi như một phương thức đồng bộ, mặt dùng có modifier async. Trình biên dịch chỉ đưa ra cảnh báo cho các phương thức như vậy.

async và await là từ khóa theo ngữ cảnh. Để có thêm nhiều thông và ví dụ, xem thêm ở awaitasync nhé.

Kiểu trả về và tham số

Một phương thức bất đồng bộ thông thường trả về một Task hoặc một Task<TResult>. Trong phương thức bất đồng bộ, một toán tử await được áp dụng cho một task mà nó trả về từ một cuộc gọi đến một phương thức bất đồng bộ khác.

Bạn xác định Task<TResult> như kiểu trả về nếu phương thức gồm một câu lệnh trả về xác định một toán hạng của kiểu TResult.

Bạn sử dụng Task như kiểu trả về nếu phương thức không có câu lệnh trả về hay có một câu lệnh trả về không trả về toán hạng.

Bắt đầu từ C# 7.0 bạn có thể xác định bất kỳ kiểu trả về, được cung cấp kiểu bao một phương thức GetAwaiter. ValueTask<TResult> là một ví dụ của kiểu như vậy. Nó có sẵn ở Nuget package System.Threading.Tasks.Extension

Ví dụ dưới đây cho thấy bạn khai báo và gọi một phương thức mà trả về một Task<TResult> hoặc một Task:

Mỗi task trả về đại diện một đống thứ đang chạy. Một task đóng gói thông tin về trạng thái của quá trình bất đồng bộ và, cuối cùng, kết quả cuối cùng từ quá trình hoặc ngoại lệ mà quá trình đó raise lên nếu nó không thành công.

Một phương thức có thể có một kiểu là trả về void. Kiều này là được dùng chính để diễn tả xử lý sự kiện, khi một kiểu trả về void được yêu cầu. Xử lý sự kiện bất đồng bộ thường phục vụ như điểm bắt đầu cho chương trình bất đồng bộ.

Một phương thức bất đồng bộ có kiểu trả về void không thể được chờ, và chỗ gọi phương thức đó sẽ không thể bắt được bất kỳ ngoại lệ nào mà phương thức đó quăng ra.

Một phương thức bất đồng bộ không thể khai báo tham số in, ref, hoặc out, nhưng phương thức có thể gọi những phương thức như những tham số đó. Thông thường, một phương thức bất đồng bộ không thể trả về một giá trị tham chiếu, mặc dù nó có thể gọi phương thức với giá trị tham chiếu trả về.

Để có thêm thông tin và ví dụ, đọc Async return types (C#). Để có nhiều thông tin hơn về cách bắt ngoại lệ trong phương thức bất đồng bộ, xem try-catch

Quy ước đặt tên

Theo quy ước, phương thức mà kiểu chờ trả về phổ biến (ví dụ Task, Task<T>, ValueTask, ValueTask<T>) nên được kết thúc với "Async". Phương thức bắt đầu một hoạt động bất đồng bộ nhưng không trả về kiểu chờ không nên được đặt tên cuối cùng là "Async", nhưng có thể bắt đầu với "Begin" hoặc "Start", hoặc một vài đông từ khác để gợi ý phương thức này không trả về hoặc quăng kết quả của hoạt động.

Bạn có thể mặc kệ quy ước khi viết một sự kiện, lớp cơ bản hay một interface gợi ý một tên khác. Ví dụ, bạn không nên đổi tên các trình xử lý sự kiện phổ biến, chẳng hạn như OnButtonClick

Nguồn: Task asynchronous programming model

Nhận xét

Bài đăng phổ biến