Task chứa gì?

Giới thiệu

Task trong .NET đã trở nên rất phổ biến. Hiện nay, chúng ta đều biết rằng để làm cho ứng dụng và dịch vụ có khả năng mở rộng tốt hơn — không nhất thiết là chạy nhanh hơn, vì đây là hai khái niệm khác nhau — chúng ta cần sử dụng chúng.

Một task là một tác vụ được thiết kế để chạy ở chế độ nền và có thể có hoặc không trả về giá trị. Task cho phép thực thi các tác vụ phức tạp, như tác vụ phụ thuộc I/O hoặc CPU, mà không chặn luồng chính. Chúng được triển khai bởi các lớp Task và Task<T>.

Trong thuật ngữ toán học và lập trình hàm, đây thực chất là các monad, nghĩa là chúng có một số tính chất nhất định mà bài viết này không đề cập.

Có hai cách gọi một phương thức được thiết kế để xử lý trên một luồng khác — phương thức trả về Task hoặc Task<T>, trong đó Task<T> kế thừa từ Task nhưng bao bọc một kết quả:

  • Sử dụng await để chờ phương thức được gọi hoàn tất.
  • Bỏ qua await để phương thức được xử lý ở chế độ nền mà không phải chờ. Cách này đôi khi được gọi là mẫu fire-and-forget.

Chờ hoàn tất

Khi gọi await trên một phương thức trả về Task, luồng gọi sẽ chờ phương thức được gọi hoàn tất. Tuy nhiên, luồng không bị chặn theo nghĩa thực tế; nó vẫn có thể thực hiện công việc khác khi cần. Khi mọi việc diễn ra đúng như dự kiến, chúng ta có thể truy cập kết quả của task.

Đây thường là điều chúng ta mong muốn, nhưng vẫn có một lựa chọn khác.

Không chờ hoàn tất

Nếu gán giá trị trả về của phương thức bất đồng bộ cho một biến kiểu Task hoặc Task<T> nhưng bỏ qua await, phương thức sẽ chạy ở chế độ nền và hoàn tất vào một thời điểm sau đó. Đây là mẫu fire-and-forget.

Vấn đề là chúng ta nên tự kiểm tra trạng thái của task. Nói cách khác, chúng ta không thực sự quên nó.

Cần kiểm tra biến Task hoặc Task<T> để biết:

  • Tác vụ đã hoàn tất hay chưa, thông qua thuộc tính Task.IsCompleted.
  • Tác vụ có bị hủy hay không, thông qua Task.IsCanceled.
  • Có ngoại lệ nào phát sinh trong quá trình xử lý hay không, thông qua Task.Exception.

Nếu task đã hoàn tất, chúng ta có thể lấy kết quả. Với Task<T>, kết quả được lưu trong Task<T>.Result.

Mọi ngoại lệ phát sinh trong khi thực thi sẽ nằm trong Task.Exception. Một AggregateException có thể bao bọc nhiều ngoại lệ khác.

Khi bỏ qua từ khóa await, trình biên dịch sẽ đưa ra cảnh báo:

CS4014: Vì lệnh gọi này không được await, phương thức hiện tại sẽ tiếp tục thực thi trước khi lệnh gọi hoàn tất. Hãy cân nhắc áp dụng toán tử await cho kết quả của lệnh gọi.

Có thể bỏ qua hoặc tắt cảnh báo này, miễn là chúng ta hiểu rõ mình đang làm gì. Những vấn đề chính gồm:

  • Quên kiểm tra IsCompleted và IsCanceled trước khi truy cập Result. Điều này có thể dẫn đến một lệnh gọi bị chặn hoặc một ngoại lệ.
  • Gọi GetAwaiter().GetResult(). Nếu task chưa hoàn tất, lệnh gọi này sẽ thực sự chặn luồng gọi.
  • Không kiểm tra Exception, dẫn đến ngoại lệ không được quan sát.

Có thể đọc thêm về chủ đề này tại đây.