Thao tác bất đồng bộ là phần không thể thiếu của bất kỳ ứng dụng nào. Trong C#, hầu hết các phương thức bất đồng bộ trả về Task hoặc Task<T>, nhưng cũng có trường hợp đặc biệt là async void. Chúng ta biết void dành cho các thao tác đồng bộ, vậy tại sao C# lại cho phép async void? Tôi sẽ phân tích câu hỏi này trong bài viết hôm nay bằng cách xem xét nhu cầu và mối nguy hiểm của async void.
Mục lục
async void là gì?
Trong C#, async void là một chữ ký phương thức không trả về gì khi hoàn thành hoặc xảy ra ngoại lệ. Từ khóa async cho phép bạn đánh dấu một phương thức là một thao tác bất đồng bộ, không chặn. Tuy nhiên, không giống như đối tác phổ biến của nó async Task hoặc async Task<T>, bạn không thể await một phương thức async void, và người gọi không thể quan sát hoặc quản lý việc thực thi. Bạn cũng không nhận được bất cứ thứ gì trả về, vì vậy không có kết quả hoặc ngoại lệ nào có thể thu được để sử dụng tiếp.
Tại sao async void nguy hiểm
Vấn đề chính với async void là không thể nhận được ngoại lệ. Hãy hiểu điều này với sự trợ giúp của một ví dụ.
Xử lý ngoại lệ trong phương thức async Task:
static async Task DangerousTaskMethod()
{
await Task.Delay(500);
throw new InvalidOperationException("Boom from async Task!");
}
Xử lý ngoại lệ trong phương thức async void:
static async void DangerousVoidMethod()
{
await Task.Delay(500);
throw new InvalidOperationException("Boom from async void!");
}
Code gọi:
Console.WriteLine("\n=== async void example ===");
try
{
DangerousVoidMethod(); // fire-and-forget
await Task.Delay(2000); // cho thời gian để ngoại lệ xuất hiện
Console.WriteLine("After void method");
}
catch (Exception ex)
{
Console.WriteLine("Caught exception from async void: " + ex.Message);
}
Đầu tiên, chúng ta gọi một phương thức async void, sau đó là async Task. Mặc dù chúng ta giữ mỗi cái trong try/catch, phương thức DangerousVoidMethod không xử lý ngoại lệ và ứng dụng bị crash.
Phương thức async hoạt động như thế nào bên trong
Khi bạn đánh dấu một phương thức async Task, trình biên dịch thực hiện những việc sau:
- Tạo một state machine ẩn dưới hood.
- Chia phương thức tại mỗi
await. - Bọc continuation trong một
Task(hoặcTask<T>) để runtime có thể theo dõi việc hoàn thành và ngoại lệ.
Tuy nhiên, khi bạn tạo một phương thức async void, không có Task nào được trả về. Người gọi không thể await, kiểm tra hoàn thành hoặc bắt ngoại lệ. Thay vào đó, các ngoại lệ được “đăng” đến SynchronizationContext hiện tại (ví dụ: UI thread dispatcher) thay vì chảy ngược lại người gọi.
Không Cách Nào Để Await Hoàn Thành
Bạn không thể await một phương thức async void. Nếu bạn thử:
await DangerousVoidMethod();
Nó sẽ hiển thị lỗi. Điều này là vì:
- Không có cách nào để theo dõi việc hoàn thành như trong Task.
- Bạn không thể chain công việc bất đồng bộ sau nó.
- Nếu nó thất bại, bạn không thể dễ dàng thử lại.
Khó Khăn Hơn Trong Unit Test
Unit test dựa vào việc await các phương thức đồng bộ. Vì các phương thức async void không thể được await, bạn không thể phát hiện việc hoàn thành và lan truyền ngoại lệ.
Mất Thông Tin
Phương thức async void vẫn giữ công việc của từ khóa async, có nghĩa là các thao tác sẽ là bất đồng bộ. Tuy nhiên, một phương thức async void không trả về giá trị. Điều này khiến bạn không thể truy cập kết quả của phương thức, cho dù nó trả về kết quả hay ngoại lệ. Việc mất thông tin làm gián đoạn sự phối hợp giữa các thao tác bất đồng bộ nơi các thao tác khác cần kết quả của các phương thức async void.
Khi nào async void có thể chấp nhận
Chúng ta đã biết những mối nguy hiểm của nó, nhưng câu hỏi chính nảy sinh: nếu nó tạo ra quá nhiều vấn đề, tại sao tính năng này lại tồn tại? Vì vậy, giống như bất kỳ công cụ hoặc tính năng nào, async void có những kịch bản mà nó phù hợp.
1. Event handlers
Events trong .NET mong đợi một kiểu trả về void. Nếu bạn cần thực hiện công việc bất đồng bộ trong một event handler, bạn chỉ còn lại async void:
private async void Button_Click(object Sender, EventArgs e)
{
await Task.Delay(1000);
MessageBox.Show("Done!");
}
2. Top-level fire-and-forget
Trong những trường hợp hiếm hoi, bạn có thể cố ý sử dụng async void. Vì hành vi fire-and-forget của nó, async void có thể hoạt động được trong các ứng dụng như logging hoặc telemetry, nơi thất bại không quan trọng và bạn không quan tâm đến việc hoàn thành.
public async void LogInFileAsync(string message)
{
await File.AppendAllTextAsync("web-log.txt", message);
}
⚠️ Lưu ý: async void trong ngữ cảnh này là rủi ro. Một cách tiếp cận an toàn hơn là bọc công việc trong Task.Run và loại bỏ kết quả:
public void LogInFile(string message)
{
_ = Task.Run(async () =>
{
try
{
await File.AppendAllTextAsync("web-log.txt", message);
}
catch (Exception ex)
{
// tùy chọn xử lý hoặc ghi log ngoại lệ
}
});
}
Thực hành tốt nhất khi làm việc với phương thức async void trong C#
Ưu tiên async Task trong hầu hết trường hợp
Vì async void không trả về ngoại lệ, nó có thể dẫn đến sự cố ứng dụng. Vì vậy, hãy ưu tiên async Task nói chung, đặc biệt trong các trường hợp mà xử lý ngoại lệ và khôi phục lỗi là quan trọng. Các phương thức async Task cung cấp cho bạn một cơ chế xử lý ngoại lệ tích hợp sẵn.
Xử lý ngoại lệ thủ công với async void
Khi bạn buộc phải dựa vào async void, hãy viết try/catch thủ công trong các phương thức async void. Phương pháp này hơi dài dòng, nhưng bạn có thể tránh ứng dụng hoạt động không mong muốn do lỗi không được xử lý.
Log và giám sát async void
Khi làm việc với các phương thức async void, hãy theo dõi chúng với logging thích hợp để bắt kịp thời bất kỳ ngoại lệ nào.
Kết luận
async void là một tùy chọn trong lập trình bất đồng bộ C#. Tuy nhiên, nó có một tập hợp các kịch bản cụ thể nơi nó hỗ trợ các nhà phát triển. Nếu tính năng này không được sử dụng đúng cách, nó có thể gây ra sự cố ứng dụng và để lại hành vi không mong muốn. Chúng ta đã khám phá những mối nguy hiểm và lý do cốt lõi của chúng trong bài viết với các ví dụ phù hợp. Chúng ta cũng đã thảo luận về các kịch bản nơi async void thực sự hữu ích. Hãy xem xét các thực hành tốt nhất để sử dụng công cụ này đúng cách.



