IHostedService vs. BackgroundService: Hiểu rõ sự khác biệt trong ASP.NET Core

Khi làm việc với ASP.NET Core, chúng ta thường gặp hai khái niệm quan trọng để xử lý các tác vụ chạy ngầm: IHostedServiceBackgroundService. Bài viết này sẽ giúp bạn hiểu rõ sự khác biệt giữa chúng và khi nào nên sử dụng từng loại.

IHostedService

Hosted services là các lớp đã triển khai interface IHostedService và cung cấp hai phương thức: StartAsyncStopAsync.


public interface IHostedService
{
Task StartAsync(CancellationToken cancellationToken);
Task StopAsync(CancellationToken cancellationToken);
}

Xem xét ví dụ này, nó chỉ có một độ trễ để mô phỏng quá trình chạy và sau đó ghi log tên của hosted service:


internal sealed class MainHostedService (ILogger<MainHostedService> logger) :IHostedService
{
public async Task StartAsync(CancellationToken cancellationToken)
{
await Task.Delay(TimeSpan.FromSeconds(2), cancellationToken);
logger.LogInformation( "Hosted Service {Service} @ {Time}" , nameof(MainHostedService), DateTime.Now);
}

public Task StopAsync(CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
}

Để ASP.NET framework và runtime biết về hosted services, chúng ta cần đăng ký chúng trong DI container:


builder.Services.AddHostedService<MainHostedService>();

Khi chúng ta làm điều này, khi khởi động ứng dụng, ASP.NET kiểm tra xem có bất kỳ triển khai IHostedService nào được đăng ký hay không, và nếu có, nó sẽ chạy chúng và chờ đợi chúng hoàn thành tác vụ. Điều này có nghĩa là cho đến khi tất cả hosted services hoàn thành tác vụ, ứng dụng sẽ không khởi động.

Trong trường hợp ứng dụng web, điều này có nghĩa là không có endpoint nào khả dụng, hoặc không có nội dung HTML nào có thể được phục vụ cho client. Nếu chúng ta chạy ứng dụng, chúng ta sẽ thấy rằng đầu tiên hosted service chạy và khi nó hoàn thành tác vụ, API mới bắt đầu lắng nghe ở địa chỉ và cổng cụ thể, và ứng dụng đã khởi động!

Hành vi này hữu ích trong các tình huống mà chúng ta cần thực hiện một số tác vụ trước khi ứng dụng sẵn sàng phục vụ yêu cầu, một ví dụ rất phổ biến là áp dụng migrations cơ sở dữ liệu Entity Framework Core.

BackgroundService

BackgroundService là một lớp trừu tượng đã triển khai interface IHostedService và cung cấp một phương thức ExecuteAsync.


public abstract class BackgroundService : IHostedService, IDisposable
{
protected abstract Task ExecuteAsync(CancellationToken stoppingToken);

// Các phương thức khác được triển khai...
}

Để framework biết về chúng, chúng ta có thể thêm chúng vào DI container theo cách chính xác giống như hosted services bằng phương thức mở rộng AddHostedService.


internal sealed class MyFirstBackGroundService(ILogger<MyFirstBackGroundService> logger) : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
await Task.Delay(TimeSpan.FromSeconds(2), stoppingToken);
logger.LogInformation("Background Service {Service} @ {Time}", nameof(MyFirstBackGroundService),
DateTime.Now);
}
}
}

Có hai điểm quan trọng cần lưu ý:

  • Tôi sử dụng vòng lặp while bên trong background service để minh họa một tác vụ chạy dài, khác với những gì chúng ta đã triển khai cho hosted services!
  • Ứng dụng, không giống như tình huống hosted service, ngay lập tức sẵn sàng phục vụ yêu cầu và không chờ background service hoàn thành tác vụ của nó.

Phương thức ExecuteAsync được triển khai theo cách fire-and-forget (bắn và quên), bạn có thể thấy triển khai của nó trong source code.

Những điều cần lưu ý

Điều gì sẽ xảy ra nếu tôi có sự kết hợp của cả IHostedServiceBackgroundService? Câu trả lời rất đơn giản, ứng dụng sẽ chạy tất cả chúng, nhưng trước khi sẵn sàng phục vụ bất kỳ yêu cầu nào, nó đảm bảo rằng tất cả hosted services đã hoàn thành tác vụ của chúng! Còn background services? Chà, không quan trọng liệu chúng đã hoàn thành hay chưa, và đây là một trong những điểm khác biệt lớn nhất giữa IHostedServiceBackgroundService, cái sau phù hợp hơn cho các quy trình chạy dài và cái trước cho các tác vụ chạy ngắn!

Một điều tôi chưa đề cập đến là thứ tự mà bất kỳ hosted services và background services nào được đăng ký trong DI container có quan trọng. Nếu chúng ta đăng ký background service trước hosted service, điều đó có nghĩa là framework sẽ chạy background service trước, KHÔNG CHỜ đợi phương thức ExecuteAsync của nó hoàn thành, và sau đó chạy hosted service chịu trách nhiệm cho migrations! Điều này sẽ dẫn đến lỗi runtime trong background service và dẫn đến việc kết thúc ứng dụng không mong muốn!

Ngoại lệ có thể xảy ra

Một sự khác biệt chính là cách hai thành phần này xử lý khi xảy ra ngoại lệ. Vì host chờ HostedServices hoàn thành công việc, bất kỳ ngoại lệ nào xảy ra ở đó sẽ ngăn ứng dụng khởi động. Tình huống hơi khác với BackgroundService:

Trong các phiên bản .NET trước, khi một ngoại lệ được ném ra từ phương thức ExecuteAsync, ngoại lệ bị mất và service có vẻ không phản hồi. Host tiếp tục chạy và không có thông báo nào được ghi log. Bắt đầu từ .NET 6, khi một ngoại lệ được ném ra từ phương thức ExecuteAsync, ngoại lệ được ghi log vào ILogger hiện tại. Theo mặc định, host bị dừng khi gặp ngoại lệ không được xử lý.

Một hành động được đề xuất là nếu bạn muốn ứng dụng không dừng khi xảy ra ngoại lệ không được xử lý trong background services, hãy sử dụng HostOptions để thay đổi hành vi đó:


builder.Host.ConfigureHostOptions(options => options.BackgroundServiceExceptionBehavior = BackgroundServiceExceptionBehavior.Ignore);

Kết luận

Cuối cùng, hãy nhớ rằng IHostedService phù hợp cho các tác vụ chạy ngắn và nó ngăn ứng dụng khởi động cho đến khi tất cả các service được đăng ký hoàn thành tác vụ của chúng. Nếu xảy ra ngoại lệ, ứng dụng sẽ không khởi động.

Mặt khác, BackgroundServices rất tuyệt cho các tác vụ chạy dài và chúng không được chờ đợi để hoàn thành tác vụ! Bạn có thể thay đổi hành vi của chúng trong trường hợp xảy ra ngoại lệ bằng cách thiết lập thuộc tính BackgroundServiceExceptionBehavior trên instance HostOptions.

Hy vọng bài viết này hữu ích với bạn. Chúc bạn coding vui vẻ!

Tài nguyên tham khảo

Chỉ mục