找回密码
 立即注册
首页 业界区 业界 如何正确实现一个 BackgroundService

如何正确实现一个 BackgroundService

梅克 2025-8-3 22:58:11
相信大家都知道如何在 .NET 中执行后台(定时)任务。首先我们会选择实现 IHostedService 接口或者继承BackgroundService 来实现后台任务。然后注册到容器内,然后注册到容器内,之后这些后台任务 service 就会自动被 触发(trigger)。本文不是初级的入门教程,而是试图告诉读者一些容易被忽略的细节。
IHostedService

IHostedService 是一个.NET Core 的接口,用于实现后台服务。通过实现这个接口,你可以在应用程序运行期间在后台执行任务,例如定时任务、监听事件、处理队列等。IHostedService 提供了 StartAsync() 和 StopAsync() 方法,分别用于启动和停止后台服务,并且框架会根据应用程序的生命周期自动调用这两个方法。
以下是这个接口的源码:
其中 StartAsync 方法由 IApplicationLifetime.ApplicationStarted 事件触发
其中 StopAsync 方法由 IApplicationLifetime.ApplicationStopped 事件触发
  1. //
  2. // 摘要:
  3. //     Defines methods for objects that are managed by the host.
  4. public interface IHostedService
  5. {
  6.   
  7.      Task StartAsync(CancellationToken cancellationToken);
  8.      Task StopAsync(CancellationToken cancellationToken);
  9. }
复制代码
通常我们的后台任务会被框在一个while循环里,定时去执行某些逻辑。以下是我们模拟的一段演示代码。StartAsync 方法被 call 的时候就会执行这个 while。代码很简单,不过多解释。
  1.     public class HostServiceTest_A : IHostedService
  2.     {
  3.         public async Task StartAsync(CancellationToken cancellationToken)
  4.         {
  5.             Console.WriteLine("HostServiceTest_A starting.");
  6.             while (!cancellationToken.IsCancellationRequested)
  7.             {
  8.                 // Simulate some work
  9.                 Console.WriteLine("HostServiceTest_A is doing work.");
  10.                 await Task.Delay(3000, cancellationToken); // Delay for 3 second
  11.             }
  12.         }
  13.         public Task StopAsync(CancellationToken cancellationToken)
  14.         {
  15.             // to do
  16.             return Task.CompletedTask;
  17.         }
  18.     }
复制代码
把这个服务注册到容器内。
  1.     builder.Services.AddHostedService<HostServiceTest_A>();
复制代码
下面让我们启动一下程序试试。可以看到程序可以启动,这个 while 循环也是一直在工作。咋看好像没啥问题,但是仔细看看的话好像缺了点什么。
问题

对了,我们这个 ASP.NET Core 程序启动日志没有了。也就是整个程序的启动过程被 block 住了。原因在于 HostedService 是顺序的,一旦某个 HostedService 的 StartAsync 方法没有尽快 return 的话,后面所有的任务全部不能执行了。比如你注册了多个 HostedService,第一个使用了这种错误的方法来执行任务,后面的 HostedService 全部都没有机会被执行。
  1. HostServiceTest_A starting.
  2. HostServiceTest_A is doing work.
  3. HostServiceTest_A is doing work.
  4. HostServiceTest_A is doing work.
  5. HostServiceTest_A is doing work.
  6. ···
复制代码
下面让我们改进一下,使用 Task.Run 来让这个任务变成异步,并且不去 await 这个 task。
  1.     public class HostServiceTest_A : IHostedService
  2.     {
  3.         public Task StartAsync(CancellationToken cancellationToken)
  4.         {
  5.             Console.WriteLine("HostServiceTest_A starting.");
  6.             Task.Run(async () => {
  7.                 while (!cancellationToken.IsCancellationRequested)
  8.                 {
  9.                     // Simulate some work
  10.                     Console.WriteLine("HostServiceTest_A is doing work.");
  11.                     await Task.Delay(3000, cancellationToken); // Delay for 3 second
  12.                 }
  13.             });
  14.             return Task.CompletedTask;
  15.         }
  16.         public Task StopAsync(CancellationToken cancellationToken)
  17.         {
  18.             return Task.CompletedTask;
  19.         }
  20.     }
复制代码
再次执行一下程序,可以看到 HostedService 跟 ASP.NET Core 主程序都可以正确执行了。
  1. HostServiceTest_A starting.
  2. HostServiceTest_A is doing work.
  3. info: Microsoft.Hosting.Lifetime[14]
  4.       Now listening on: http://localhost:5221
  5. info: Microsoft.Hosting.Lifetime[0]
  6.       Application started. Press Ctrl+C to shut down.
  7. info: Microsoft.Hosting.Lifetime[0]
  8.       Hosting environment: Development
  9. info: Microsoft.Hosting.Lifetime[0]
  10.       Content root path: D:\workspace\BackgroundServiceDemo\BackgroundServiceDemo
  11. HostServiceTest_A is doing work.
复制代码
改进

我们的后台任务通常是一个长期任务,这种情况下更加推荐 LongRunning Task 来 handle 这种任务。至于为什么可以参考以下文档:
https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.taskcreationoptions?view=net-9.0
  1.            Task.Factory.StartNew(async () => {
  2.                while (!cancellationToken.IsCancellationRequested)
  3.                {
  4.                    // Simulate some work
  5.                    Console.WriteLine("HostServiceTest_A is doing work.");
  6.                    await Task.Delay(3000, cancellationToken); // Delay for 3 second
  7.                }
  8.            }, TaskCreationOptions.LongRunning);
  9.            return Task.CompletedTask;
复制代码
退出

以上我们都在说如何启动后台任务,还没讨论如何取消这个后台任务。参入的那个 cancellationToken 在 Application 被 stop 的时候并不会主动 cancel。所以我们需要在 StopAsync 方法触发的时候手动来 Cancel 这个 token。
  1.     public class HostServiceTest_A : IHostedService
  2.     {
  3.         private CancellationTokenSource _cancellationTokenSource;
  4.         public Task StartAsync(CancellationToken cancellationToken)
  5.         {
  6.             Console.WriteLine("HostServiceTest_A starting.");
  7.             _cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
  8.             Task.Factory.StartNew(async () => {
  9.                 while (!_cancellationTokenSource.Token.IsCancellationRequested)
  10.                 {
  11.                     // Simulate some work
  12.                     Console.WriteLine("HostServiceTest_A is doing work.");
  13.                     await Task.Delay(1000, cancellationToken); // Delay for 3 second
  14.                 }
  15.                 Console.WriteLine("HostServiceTest_A task done.");
  16.             }, TaskCreationOptions.LongRunning);
  17.             return Task.CompletedTask;
  18.         }
  19.         public Task StopAsync(CancellationToken cancellationToken)
  20.         {
  21.             if (!cancellationToken.IsCancellationRequested)
  22.             {
  23.                 _cancellationTokenSource.Cancel();
  24.             }
  25.             Console.WriteLine("HostServiceTest_A stop.");
  26.             return Task.CompletedTask;
  27.         }
  28.     }
复制代码
让我们运行一下,然后按下 Ctrl + C 来主动退出程序,可以看到我们的 while 被安全退出了。
  1. HostServiceTest_A starting.
  2. HostServiceTest_A is doing work.
  3. info: Microsoft.Hosting.Lifetime[14]
  4.       Now listening on: http://localhost:5221
  5. info: Microsoft.Hosting.Lifetime[0]
  6.       Application started. Press Ctrl+C to shut down.
  7. info: Microsoft.Hosting.Lifetime[0]
  8.       Hosting environment: Development
  9. info: Microsoft.Hosting.Lifetime[0]
  10.       Content root path: D:\workspace\BackgroundServiceDemo\BackgroundServiceDemo
  11. HostServiceTest_A is doing work.HostServiceTest_A is doing work.HostServiceTest_A is doing work.info: Microsoft.Hosting.Lifetime[0]      Application is shutting down...HostServiceTest_A stop.HostServiceTest_A task done.
复制代码
BackgroundService

除了,HostedService,微软还给我们提供了 BackgroundService 这个类。一看这个类名就知道他能干嘛。其实也未必想的这么简单。BackgroundService 实际上是 IHostedService 的一个实现类。它的核心是将后台任务逻辑放在 ExecuteAsync 这个抽象方法中。下面我们通过一个具体案例来分析。。
  1.     public class BackgroundServiceTest_A : BackgroundService
  2.     {
  3.         protected override async Task ExecuteAsync(CancellationToken stoppingToken)
  4.         {
  5.             while (!stoppingToken.IsCancellationRequested)
  6.             {
  7.                 Console.WriteLine("ExecuteAsyncA is running.");
  8.                 await Task.Delay(3000);
  9.             }
  10.         }
  11.     }
复制代码
运行这个代码,可以看到 BackgroundService 正常启动了,而且也没 block 住 ASP.NET Core 的程序。看是一切完美。
  1. ExecuteAsyncA is running.
  2. info: Microsoft.Hosting.Lifetime[14]
  3.       Now listening on: http://localhost:5221
  4. info: Microsoft.Hosting.Lifetime[0]
  5.       Application started. Press Ctrl+C to shut down.
  6. info: Microsoft.Hosting.Lifetime[0]
  7.       Hosting environment: Development
  8. info: Microsoft.Hosting.Lifetime[0]
  9.       Content root path: D:\workspace\BackgroundServiceDemo\BackgroundServiceDemo
  10. ExecuteAsyncA is running.
  11. ExecuteAsyncA is running.
  12. ExecuteAsyncA is running.
  13. ExecuteAsyncA is running.
  14. ExecuteAsyncA is running.
复制代码
问题

以上代码真的没有问题吗?其实不尽然。让我们上点强度。如果我们在循环中加一个耗时很长的步骤。事实上这个很常见。比如以下代码:
  1.     public class BackgroundServiceTest_A : BackgroundService
  2.     {
  3.         protected override async Task ExecuteAsync(CancellationToken stoppingToken)
  4.         {
  5.             while (!stoppingToken.IsCancellationRequested)
  6.             {
  7.                 Console.WriteLine("ExecuteAsyncA is running.");
  8.                 LongTermTask();
  9.                 await Task.Delay(3000);
  10.             }
  11.         }
  12.         private void LongTermTask()
  13.         {
  14.             // Simulate some work
  15.             Console.WriteLine("LongTermTaskA is doing work.");
  16.             Thread.Sleep(30000);
  17.         }
  18.     }
复制代码
再次运行以下,我们可以发现 ASP.NET Core 的主程序起不来了,被 block 住了。只有等第一个循环周期过后,主程序才能启动起来。
  1. ExecuteAsyncA is running.
  2. LongTermTaskA is doing work.
复制代码
那么问题到底出在哪?让我们看看 BackgroundService 的源码。
  1.         public virtual Task StartAsync(CancellationToken cancellationToken)
  2.         {
  3.             // Create linked token to allow cancelling executing task from provided token
  4.             _stoppingCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
  5.             // Store the task we're executing
  6.             _executeTask = ExecuteAsync(_stoppingCts.Token);
  7.             // If the task is completed then return it, this will bubble cancellation and failure to the caller
  8.             if (_executeTask.IsCompleted)
  9.             {
  10.                 return _executeTask;
  11.             }
  12.             // Otherwise it's running
  13.             return Task.CompletedTask;
  14.         }
复制代码
可以看到 StartAsync 方法会调用 ExecuteAsync,但是它没有 await 这个方法,也就是说 StartAsync 内部实现是个同步方法。也就是说 ExecuteAsync 方法跟 StartAsync 会在同一个线程上被执行(在遇到第一个 await 之前)。如果你注册了多个 BackgroundService 并且他们一次 loop 都非常耗时,那么这个程序启动将会非常耗时。其实微软已经在文档上提醒大家了:
Avoid performing long, blocking initialization work in ExecuteAsync.
改进

那么改进方法,同样使用 Task.Factory.StartNew 来构造一个  LongRunning 的 task 就可以解决。
  1.     public class BackgroundServiceTest_A : BackgroundService
  2.     {
  3.         protected override Task ExecuteAsync(CancellationToken stoppingToken)
  4.         {
  5.             return Task.Factory.StartNew(async () =>
  6.             {
  7.                 while (!stoppingToken.IsCancellationRequested)
  8.                 {
  9.                     // Simulate some work
  10.                     Console.WriteLine("HostServiceTest_A is doing work.");
  11.                     LongTermTask();
  12.                     await Task.Delay(1000, stoppingToken); // Delay for 1 second
  13.                 }
  14.                 Console.WriteLine("HostServiceTest_A task done.");
  15.             }, TaskCreationOptions.LongRunning);
  16.         }
  17.         private void LongTermTask()
  18.         {
  19.             // Simulate some work
  20.             Console.WriteLine("LongTermTaskA is doing work.");
  21.             Thread.Sleep(30000);
  22.         }
  23.     }
复制代码
运行一下,完美启动后台任务跟主程序。
  1. HostServiceTest_A is doing work.
  2. LongTermTaskA is doing work.
  3. info: Microsoft.Hosting.Lifetime[14]
  4.       Now listening on: http://localhost:5221
  5. info: Microsoft.Hosting.Lifetime[0]
  6.       Application started. Press Ctrl+C to shut down.
  7. info: Microsoft.Hosting.Lifetime[0]
  8.       Hosting environment: Development
  9. info: Microsoft.Hosting.Lifetime[0]
  10.       Content root path: D:\workspace\BackgroundServiceDemo\BackgroundServiceDemo
复制代码
继续改进

如果要继续吹毛求疵的话,我们还可以改进一下。从 .NET6 开始 PeriodicTimer 被加入进来。它是一个 timer,可以替换一部分 Task.Delay 活。使用 PeriodicTimer 话相对于 Task.Delay 来说可以让 loop 的间隔更加精准的被控制。
详见这里 https://learn.microsoft.com/en-us/dotnet/api/system.threading.periodictimer.waitfornexttickasync?view=net-9.0
  1.      protected override Task ExecuteAsync(CancellationToken stoppingToken)
  2.      {
  3.          return Task.Factory.StartNew(async () =>
  4.          {
  5.              var timer = new PeriodicTimer(TimeSpan.FromSeconds(1));
  6.              while (await timer.WaitForNextTickAsync(stoppingToken))
  7.              {
  8.                  // Simulate some work
  9.                  Console.WriteLine("HostServiceTest_A is doing work.");
  10.                  LongTermTask();
  11.              }
  12.              Console.WriteLine("HostServiceTest_A task done.");
  13.          }, TaskCreationOptions.LongRunning);
  14.      }
复制代码
总结

通过以上的演示,我们可以感受到,实现一个后台任务还是有非常多的点需要被注意的。特别是不要在 StartAsync 或者 ExcuteAsync 方法内执行耗时的同步方法。如果有耗时任务请包裹在新的 Task 内执行。我们要保证这两个方法轻量化能够被快速的执行完毕,这样的话不会影响应用程序的启动。

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
您需要登录后才可以回帖 登录 | 立即注册