async và await giúp ứng dụng .NET xử lý nhiều công việc chờ I/O mà không giữ một luồng trong trạng thái bị chặn. Trong website, phần lớn thời gian của request có thể dành để chờ cơ sở dữ liệu, API, tệp hoặc dịch vụ bên ngoài. Dùng bất đồng bộ đúng cách giúp máy chủ phục vụ nhiều request đồng thời hơn và giữ giao diện phản hồi tốt.
Tuy nhiên, thêm từ khóa async không tự động làm chương trình nhanh hơn. Sai lầm như dùng Task.Run cho I/O, chặn bằng Result hoặc chạy tuần tự các tác vụ độc lập vẫn có thể làm giảm hiệu năng.
Đây là quyết định đầu tiên khi thiết kế mã bất đồng bộ.
await, không bọc bằng Task.Run.Task.Run để tránh khóa luồng giao diện. Trên máy chủ, cần cân nhắc giới hạn CPU và hàng đợi công việc nền.Task.Run không biến một lệnh đồng bộ truy cập cơ sở dữ liệu thành I/O bất đồng bộ thực sự. Nó chỉ chuyển công việc chặn sang một luồng khác trong thread pool.
Một luồng tốt là bất đồng bộ từ controller hoặc page handler xuống repository và HTTP client. Tránh chuyển đổi qua lại giữa đồng bộ và bất đồng bộ.
public async Task Details( int id, CancellationToken cancellationToken)
{ var product = await repository.GetByIdAsync(id, cancellationToken); if (product == null) return NotFound(); return View(product);
} Nếu tầng dưới có API GetByIdAsync, tầng trên nên await nó thay vì gọi Result hoặc Wait.
task.Result và task.Wait() giữ luồng hiện tại cho đến khi công việc hoàn tất. Trong một số môi trường, cách này còn có thể gây deadlock. Trong ASP.NET Core, nguy cơ deadlock do synchronization context thấp hơn ứng dụng giao diện cũ, nhưng việc chặn luồng vẫn làm giảm khả năng mở rộng.
Thay vì:
var product = repository.GetByIdAsync(id).Result;Hãy dùng:
var product = await repository.GetByIdAsync(id, cancellationToken);Nếu ba API không phụ thuộc lẫn nhau, chờ từng API theo thứ tự làm tổng thời gian gần bằng tổng của cả ba. Khởi tạo các tác vụ trước rồi dùng Task.WhenAll giúp thời gian gần với tác vụ lâu nhất.
var productTask = productService.GetAsync(id, cancellationToken);
var reviewsTask = reviewService.GetAsync(id, cancellationToken);
var stockTask = stockService.GetAsync(id, cancellationToken); await Task.WhenAll(productTask, reviewsTask, stockTask); var model = new ProductPageModel
{ Product = await productTask, Reviews = await reviewsTask, Stock = await stockTask
};Chỉ làm vậy khi các tác vụ thực sự độc lập và tài nguyên phía sau chịu được mức đồng thời. Không chạy song song nhiều truy vấn trên cùng một DbContext, vì một instance không hỗ trợ nhiều thao tác đồng thời.
Khi người dùng đóng kết nối hoặc request hết hạn, công việc phía máy chủ không còn nhiều giá trị. Truyền CancellationToken qua các tầng cho phép HTTP client, truy vấn cơ sở dữ liệu và tác vụ hỗ dẫn dừng sớm.
public async Task GetByIdAsync( int id, CancellationToken cancellationToken)
{ return await db.Products .AsNoTracking() .FirstOrDefaultAsync(x => x.Id == id, cancellationToken);
} Cancellation là luồng điều khiển bình thường, không nên luôn ghi log như một lỗi nghiêm trọng. Hãy phân biệt request bị hủy với lỗi kết nối hoặc lỗi nghiệp vụ.
Phương thức async void không thể được chờ, khó bắt lỗi và khó kiểm thử. Ngoại lệ có thể thoát khỏi luồng xử lý dự kiến. Ngoại trừ event handler bắt buộc trả về void, phương thức bất đồng bộ nên trả về Task hoặc Task.
Mỗi phương thức async có thể tạo state machine và phát sinh chi phí nhỏ. Trong phần lớn mã nghiệp vụ, tính rõ ràng quan trọng hơn vi tối ưu này. Nhưng ở đường chạy cực nóng, nếu phương thức chỉ chuyển tiếp một Task và không cần xử lý sau await, có thể trả Task trực tiếp.
public Task GetProductAsync( int id, CancellationToken cancellationToken)
{ return repository.GetByIdAsync(id, cancellationToken);
} Đừng bỏ await nếu phương thức cần try, finally, biến đổi kết quả hoặc bảo đảm ngoại lệ được quan sát tại đúng vị trí.
Không. ValueTask có thể giảm allocation khi kết quả thường xuyên có sẵn đồng bộ, nhưng kiểu này phức tạp hơn và có quy tắc sử dụng chặt chẽ hơn. Nó phù hợp với thư viện hoặc đường chạy đã được đo rõ, không phải lựa chọn mặc định cho mọi phương thức.
Task.WhenAll trên hàng nghìn mục có thể tạo một đợt tải lớn tới API hoặc cơ sở dữ liệu. Hãy giới hạn số công việc đồng thời bằng SemaphoreSlim, Parallel.ForEachAsync với mức độ song song phù hợp hoặc một hàng đợi nền.
Ví dụ xử lý nhiều URL với giới hạn:
await Parallel.ForEachAsync( urls, new ParallelOptions { MaxDegreeOfParallelism = 8, CancellationToken = cancellationToken }, async (url, token) => { await client.GetStringAsync(url, token); });Con số phù hợp phụ thuộc giới hạn dịch vụ, thời gian phản hồi, số kết nối và tài nguyên máy chủ. Hãy đo thay vì chọn theo cảm tính.
Khi nhiều tác vụ chạy đồng thời, cần quyết định nếu một tác vụ lỗi thì có hủy phần còn lại hay vẫn thu thập các kết quả thành công. await Task.WhenAll sẽ ném khi có lỗi. Hãy ghi log đầy đủ và không bỏ quên các Task đã khởi tạo.
Với tác vụ nền quan trọng, không nên khởi tạo rồi bỏ mặc trong request. Hãy đưa công việc vào hàng đợi có cơ chế retry, quan sát trạng thái và xử lý tắt ứng dụng an toàn.
Task.Run để bọc I/O đồng bộ trong request.Result hoặc Wait.CancellationToken xuyên suốt chuỗi gọi.Task.WhenAll cho các công việc độc lập.async void ngoài event handler.ValueTask.DbContext cho nhiều thao tác song song.Bất đồng bộ giúp ứng dụng web mở rộng tốt hơn khi phần lớn thời gian dành cho I/O. Hiệu quả đến từ việc dùng API async thực sự, không chặn Task, truyền cancellation và điều phối mức đồng thời hợp lý. async và await không phải phép tăng tốc mọi đoạn mã, nhưng khi đặt đúng chỗ, chúng giúp tài nguyên máy chủ được sử dụng hiệu quả và request phản hồi ổn định hơn.
Nguồn tham khảo:Asynchronous programming scenarios trong C#.
Cập nhật tin tức nhanh nhất và mới nhất