一、概述

1、概念

Worker Service 是使用模板构建的 .NET 项目,在VS2019中可以找到。Worker Service可以用来编写长时间运行的后台服务,并且能部署成windows服务或linux守护程序。Worker Service 没有用户界面,也不支持直接的用户交互,它们特别适用于设计微服务架构。在微服务体系结构中,职责通常被划分为不同的、可单独部署的、可伸缩的服务。随着微服务架构的成长和发展,拥有大量的 Worker Service 会变得越来越常见。

2、应用场景

  • 处理来自队列、服务总线或事件流的消息、事件
  • 响应对象、文件存储中的文件更改
  • 聚合数据存储中的数据
  • 丰富数据提取管道中的数据
  • 可以支持定期的批处理工作负载

二、创建Worker Service

2.1 创建新项目->选择 Worker Service

Worker Service介绍 - 图1

Worker Service介绍 - 图2

2.2 项目创建成功之后,会自动创建两个类:Program和Worker

Worker Service介绍 - 图3

2.2.1 Program.cs

  1. using Microsoft.Extensions.DependencyInjection;
  2. using Microsoft.Extensions.Hosting;
  3. using System;
  4. using System.Collections.Generic;
  5. using System.Linq;
  6. using System.Threading.Tasks;
  7. namespace WorkerService1
  8. {
  9. public class Program
  10. {
  11. public static void Main(string[] args)
  12. {
  13. CreateHostBuilder(args).Build().Run();
  14. }
  15. public static IHostBuilder CreateHostBuilder(string[] args) =>
  16. Host.CreateDefaultBuilder(args)
  17. .ConfigureServices((hostContext, services) =>
  18. {
  19. services.AddHostedService<Worker>();
  20. });
  21. }
  22. }
Program类跟ASP.NET Core Web应用程序非常类似,不同之处没有了startup类,并且把worker服务添加到DI container中。

2.2.2 Worker.cs

  1. using Microsoft.Extensions.Hosting;
  2. using Microsoft.Extensions.Logging;
  3. using System;
  4. using System.Collections.Generic;
  5. using System.Linq;
  6. using System.Threading;
  7. using System.Threading.Tasks;
  8. namespace WorkerService1
  9. {
  10. public class Worker : BackgroundService
  11. {
  12. private readonly ILogger<Worker> _logger;
  13. public Worker(ILogger<Worker> logger)
  14. {
  15. _logger = logger;
  16. }
  17. /// <summary>
  18. /// 重写BackgroundService.ExecuteAsync方法,封装windows服务或linux守护程序中的处理逻辑
  19. /// </summary>
  20. /// <param name="stoppingToken"></param>
  21. /// <returns></returns>
  22. protected override async Task ExecuteAsync(CancellationToken stoppingToken)
  23. {
  24. //如果服务被停止,那么下面的IsCancellationRequested会返回true,我们就应该结束循环
  25. while (!stoppingToken.IsCancellationRequested)
  26. {
  27. //模拟服务中的处理逻辑,这里我们仅输出一条日志,并且等待1秒钟时间
  28. _logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now);
  29. await Task.Delay(1000, stoppingToken);
  30. }
  31. }
  32. }
  33. }
worker继承自BackgroundService ,而后者又实现IHostedService接口。worker类的构造函数中,使用了.NET Core自带的日志组件接口对象ILogger,它是通过DI(依赖注入)注入到worker类的构造函数中的。ExecuteAsync 方法用来完成相应的逻辑,该方法实际上属于BackgroundService类,可以在worker类中重写(override)它。通过ExecuteAsync方法传入的CancellationToken参数对象,来判断是否应该结束循环,例如如果windows服务被停止,那么参数中CancellationToken类的IsCancellationRequested属性会返回true,ExecuteAsync方法就会停止循环,来结束整个windows服务。

2.2.3 重写BackgroundService类的StartAsync、ExecuteAsync、StopAsync方法

我们也可以在worker类中重写BackgroundService.StartAsync方法和BackgroundService.StopAsync方法,注意重写时,不要忘记在worker类中调用base.StartAsync和base.StopAsync,因为BackgroundService类的StartAsync和StopAsync会执行一些Worker Service的核心代码,在开始和结束Worker Service服务(例如开始和停止windows服务)的时候,来执行一些处理逻辑,本例中我们分别输出了一条日志:
  1. using Microsoft.Extensions.Hosting;
  2. using Microsoft.Extensions.Logging;
  3. using System;
  4. using System.Collections.Generic;
  5. using System.Linq;
  6. using System.Threading;
  7. using System.Threading.Tasks;
  8. namespace WorkerService1
  9. {
  10. public class Worker : BackgroundService
  11. {
  12. private readonly ILogger<Worker> _logger;
  13. public Worker(ILogger<Worker> logger)
  14. {
  15. _logger = logger;
  16. }
  17. //重写BackgroundService.StartAsync方法,在开始服务的时候,执行一些处理逻辑,这里我们仅输出一条日志
  18. public override async Task StartAsync(CancellationToken cancellationToken)
  19. {
  20. _logger.LogInformation("Worker starting at: {time}", DateTimeOffset.Now);
  21. await base.StartAsync(cancellationToken);
  22. }
  23. /// <summary>
  24. /// 重写BackgroundService.ExecuteAsync方法,封装windows服务或linux守护程序中的处理逻辑
  25. /// </summary>
  26. /// <param name="stoppingToken"></param>
  27. /// <returns></returns>
  28. protected override async Task ExecuteAsync(CancellationToken stoppingToken)
  29. {
  30. //如果服务被停止,那么下面的IsCancellationRequested会返回true,我们就应该结束循环
  31. while (!stoppingToken.IsCancellationRequested)
  32. {
  33. //模拟服务中的处理逻辑,这里我们仅输出一条日志,并且等待1秒钟时间
  34. _logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now);
  35. await Task.Delay(1000, stoppingToken);
  36. }
  37. }
  38. //重写BackgroundService.StopAsync方法,在结束服务的时候,执行一些处理逻辑,这里我们仅输出一条日志
  39. public override async Task StopAsync(CancellationToken cancellationToken)
  40. {
  41. _logger.LogInformation("Worker stopping at: {time}", DateTimeOffset.Now);
  42. await base.StopAsync(cancellationToken);
  43. }
  44. }
  45. }
由于BackgroundService类的StartAsync、ExecuteAsync、StopAsync方法返回的都是Task类型,我们可以使用async和await关键字将它们重写为异步函数,来提高程序的性能。运行结果如下所示,每隔1秒循环打印运行的时间(可以在启动的控制台中使用快捷键”Ctrl+C”来停止Worker Service的运行,相当于停止windows服务或linux守护程序):

Worker Service介绍 - 图4

从下图可以看到Worker Service项目从本质上来说就是一个控制台项目,只不过当它被部署为windows服务或linux守护程序后,不会显示控制台窗口。所以实际上在Visual Studio中进行调试的时候,完全可以用Console.WriteLine等控制台方法来替代ILogger接口的日志输出方法,不过由于ILogger接口的日志输出方法也可以记录到文件等媒介上,还是更推荐使用ILogger接口来输出调试信息。

Worker Service介绍 - 图5

2.2.4 避免线程阻塞

不要让线程阻塞worker类中重写的StartAsync、ExecuteAsync、StopAsync方法,因为StartAsync方法负责启动Worker Service,如果调用StartAsync方法的线程被一直阻塞了,那么Worker Service的启动就一直完成不了。同理StopAsync方法负责结束Worker Service,如果调用StopAsync方法的线程被一直阻塞了,那么Worker Service的结束就一直完成不了。这里主要说明下为什么ExecuteAsync方法不能被阻塞,我们尝试把本例中的ExecuteAsync方法改为如下代码:
  1. protected override async Task ExecuteAsync(CancellationToken stoppingToken)
  2. {
  3. while (!stoppingToken.IsCancellationRequested)
  4. {
  5. _logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now);
  6. Thread.Sleep(1000);//使用Thread.Sleep进行同步等待,调用ExecuteAsync方法的线程会一直执行这里的循环,被不停地被阻塞
  7. }
  8. await Task.CompletedTask;
  9. }
我们将ExecuteAsync方法中的异步等待方法Task.Delay,改为了同步等待方法Thread.Sleep。由于Thread.Sleep方法是将执行线程通过阻塞的方式来进行等待,所以现在调用ExecuteAsync方法的线程会一直执行ExecuteAsync方法中的循环,被不停地被阻塞,除非ExecuteAsync方法中的循环结束,那么调用ExecuteAsync方法的线程会被一直卡在ExecuteAsync方法中。现在我们在Visual Studio中运行Worker Service,执行结果如下:

Worker Service介绍 - 图6

我们可以看到当我们在控制台中使用快捷键”Ctrl+C”试图停止Worker Service后(上图红色框中输出的日志),ExecuteAsync方法中的循环还是在不停地运行来输出日志,这说明ExecuteAsync方法的CancellationToken参数的IsCancellationRequested属性还是返回的false,所以这就是问题所在,如果我们直接用调用ExecuteAsync方法的线程去做循环,来执行windows服务或linux守护程序的处理逻辑,会导致Worker Service无法被正常停止,因为ExecuteAsync方法的CancellationToken参数没有被更新。所以,那些很耗时并且要循环处理的 windows服务或linux守护程序的处理逻辑,应该要放到另外的线程中去执行,而不是由调用ExecuteAsync方法的线程去执行。所以假设我们现在有三个windows服务或linux守护程序的逻辑现在要被处理,我们可以将它们放到三个新的线程中去执行,如下代码所示:
  1. using Microsoft.Extensions.Hosting;
  2. using Microsoft.Extensions.Logging;
  3. using System;
  4. using System.Collections.Generic;
  5. using System.Linq;
  6. using System.Threading;
  7. using System.Threading.Tasks;
  8. namespace WorkerService1
  9. {
  10. public class Worker : BackgroundService
  11. {
  12. private readonly ILogger<Worker> _logger;
  13. public Worker(ILogger<Worker> logger)
  14. {
  15. _logger = logger;
  16. }
  17. //重写BackgroundService.StartAsync方法,在开始服务的时候,执行一些处理逻辑,这里我们仅输出一条日志
  18. public override async Task StartAsync(CancellationToken cancellationToken)
  19. {
  20. _logger.LogInformation("Worker starting at: {time}", DateTimeOffset.Now);
  21. await base.StartAsync(cancellationToken);
  22. }
  23. //第一个 windows服务或linux守护程序 的处理逻辑,由RunTaskOne方法内部启动的Task任务线程进行处理,同样可以从参数CancellationToken stoppingToken中的IsCancellationRequested属性,得知Worker Service服务是否已经被停止
  24. protected Task RunTaskOne(CancellationToken stoppingToken)
  25. {
  26. return Task.Run(() =>
  27. {
  28. //如果服务被停止,那么下面的IsCancellationRequested会返回true,我们就应该结束循环
  29. while (!stoppingToken.IsCancellationRequested)
  30. {
  31. _logger.LogInformation("RunTaskOne running at: {time}", DateTimeOffset.Now);
  32. Thread.Sleep(1000);
  33. }
  34. }, stoppingToken);
  35. }
  36. //第二个 windows服务或linux守护程序 的处理逻辑,由RunTaskTwo方法内部启动的Task任务线程进行处理,同样可以从参数CancellationToken stoppingToken中的IsCancellationRequested属性,得知Worker Service服务是否已经被停止
  37. protected Task RunTaskTwo(CancellationToken stoppingToken)
  38. {
  39. return Task.Run(() =>
  40. {
  41. //如果服务被停止,那么下面的IsCancellationRequested会返回true,我们就应该结束循环
  42. while (!stoppingToken.IsCancellationRequested)
  43. {
  44. _logger.LogInformation("RunTaskTwo running at: {time}", DateTimeOffset.Now);
  45. Thread.Sleep(1000);
  46. }
  47. }, stoppingToken);
  48. }
  49. //第三个 windows服务或linux守护程序 的处理逻辑,由RunTaskThree方法内部启动的Task任务线程进行处理,同样可以从参数CancellationToken stoppingToken中的IsCancellationRequested属性,得知Worker Service服务是否已经被停止
  50. protected Task RunTaskThree(CancellationToken stoppingToken)
  51. {
  52. return Task.Run(() =>
  53. {
  54. //如果服务被停止,那么下面的IsCancellationRequested会返回true,我们就应该结束循环
  55. while (!stoppingToken.IsCancellationRequested)
  56. {
  57. _logger.LogInformation("RunTaskThree running at: {time}", DateTimeOffset.Now);
  58. Thread.Sleep(1000);
  59. }
  60. }, stoppingToken);
  61. }
  62. //重写BackgroundService.ExecuteAsync方法,封装windows服务或linux守护程序中的处理逻辑
  63. protected override async Task ExecuteAsync(CancellationToken stoppingToken)
  64. {
  65. try
  66. {
  67. Task taskOne = RunTaskOne(stoppingToken);
  68. Task taskTwo = RunTaskTwo(stoppingToken);
  69. Task taskThree = RunTaskThree(stoppingToken);
  70. await Task.WhenAll(taskOne, taskTwo, taskThree);//使用await关键字,异步等待RunTaskOne、RunTaskTwo、RunTaskThree方法返回的三个Task对象完成,这样调用ExecuteAsync方法的线程会立即返回,不会卡在这里被阻塞
  71. }
  72. catch (Exception ex)
  73. {
  74. //RunTaskOne、RunTaskTwo、RunTaskThree方法中,异常捕获后的处理逻辑,这里我们仅输出一条日志
  75. _logger.LogError(ex.Message);
  76. }
  77. finally
  78. {
  79. //Worker Service服务停止后,如果有需要收尾的逻辑,可以写在这里
  80. }
  81. }
  82. //重写BackgroundService.StopAsync方法,在结束服务的时候,执行一些处理逻辑,这里我们仅输出一条日志
  83. public override async Task StopAsync(CancellationToken cancellationToken)
  84. {
  85. _logger.LogInformation("Worker stopping at: {time}", DateTimeOffset.Now);
  86. await base.StopAsync(cancellationToken);
  87. }
  88. }
  89. }
所以现在调用ExecuteAsync方法的线程就不会被阻塞了,执行结果如下:

Worker Service介绍 - 图7

可以看到这次,当我们在控制台中使用快捷键”Ctrl+C”试图停止Worker Service后,ExecuteAsync方法就立即停止运行了,所以这里再次强调千万不要去阻塞调用ExecuteAsync方法的线程!另外上面代码中,我们在worker类重写的ExecuteAsync方法中放了一个finally代码块,这个代码块可以用来执行一些Worker Service服务停止后的一些收尾代码逻辑(例如关闭数据库连接、释放资源等),我更倾向于使用ExecuteAsync方法中的finally代码块来做Worker Service的收尾工作,而不是在worker类重写的StopAsync方法中来做收尾工作,从BackgroundService的源代码,我们可以看出worker类的StopAsync方法是有可能比ExecuteAsync方法先完成的,所以Worker Service的收尾工作应该放到ExecuteAsync方法中的finally代码块,因为ExecuteAsync方法中的finally代码块,肯定是在RunTaskOne、RunTaskTwo、RunTaskThree方法返回的三个Task对象执行完毕后才执行的。

2.2.5 在Worker Service中运行多个Worker类

在前面的例子中,可以看到我们在一个Worker类中定义了三个方法RunTaskOne、RunTaskTwo、RunTaskThree,来执行三个 windows服务或linux守护程序 的逻辑。其实我们还可以在一个Worker Service项目中,定义和执行多个Worker类,而不是把所有的代码逻辑都放在一个Worker类中。首先我们定义第一个Worker类WorkerOne:
  1. using Microsoft.Extensions.Hosting;
  2. using Microsoft.Extensions.Logging;
  3. using System;
  4. using System.Threading;
  5. using System.Threading.Tasks;
  6. namespace WorkerService1
  7. {
  8. public class WorkerOne:BackgroundService
  9. {
  10. private readonly ILogger<WorkerOne> _logger;
  11. public WorkerOne(ILogger<WorkerOne> logger)
  12. {
  13. _logger = logger;
  14. }
  15. //重写BackgroundService.StartAsync方法,在开始服务的时候,执行一些处理逻辑,这里我们仅输出一条日志
  16. public override async Task StartAsync(CancellationToken cancellationToken)
  17. {
  18. _logger.LogInformation("WorkerOne starting at: {time}", DateTimeOffset.Now);
  19. await base.StartAsync(cancellationToken);
  20. }
  21. //重写BackgroundService.ExecuteAsync方法,封装windows服务或linux守护程序中的处理逻辑
  22. protected override async Task ExecuteAsync(CancellationToken stoppingToken)
  23. {
  24. //如果服务被停止,那么下面的IsCancellationRequested会返回true,我们就应该结束循环
  25. while (!stoppingToken.IsCancellationRequested)
  26. {
  27. //模拟服务中的处理逻辑,这里我们仅输出一条日志,并且等待1秒钟时间
  28. _logger.LogInformation("WorkerOne running at: {time}", DateTimeOffset.Now);
  29. await Task.Delay(1000, stoppingToken);
  30. }
  31. }
  32. //重写BackgroundService.StopAsync方法,在结束服务的时候,执行一些处理逻辑,这里我们仅输出一条日志
  33. public override async Task StopAsync(CancellationToken cancellationToken)
  34. {
  35. _logger.LogInformation("WorkerOne stopping at: {time}", DateTimeOffset.Now);
  36. await base.StopAsync(cancellationToken);
  37. }
  38. }
  39. }
接着我们定义第二个Worker类WorkerTwo:
  1. using Microsoft.Extensions.Hosting;
  2. using Microsoft.Extensions.Logging;
  3. using System;
  4. using System.Threading;
  5. using System.Threading.Tasks;
  6. namespace WorkerService1
  7. {
  8. public class WorkerTwo : BackgroundService
  9. {
  10. private readonly ILogger<WorkerTwo> _logger;
  11. public WorkerTwo(ILogger<WorkerTwo> logger)
  12. {
  13. _logger = logger;
  14. }
  15. //重写BackgroundService.StartAsync方法,在开始服务的时候,执行一些处理逻辑,这里我们仅输出一条日志
  16. public override async Task StartAsync(CancellationToken cancellationToken)
  17. {
  18. _logger.LogInformation("WorkerTwo starting at: {time}", DateTimeOffset.Now);
  19. await base.StartAsync(cancellationToken);
  20. }
  21. //重写BackgroundService.ExecuteAsync方法,封装windows服务或linux守护程序中的处理逻辑
  22. protected override async Task ExecuteAsync(CancellationToken stoppingToken)
  23. {
  24. //如果服务被停止,那么下面的IsCancellationRequested会返回true,我们就应该结束循环
  25. while (!stoppingToken.IsCancellationRequested)
  26. {
  27. //模拟服务中的处理逻辑,这里我们仅输出一条日志,并且等待1秒钟时间
  28. _logger.LogInformation("WorkerTwo running at: {time}", DateTimeOffset.Now);
  29. await Task.Delay(1000, stoppingToken);
  30. }
  31. }
  32. //重写BackgroundService.StopAsync方法,在结束服务的时候,执行一些处理逻辑,这里我们仅输出一条日志
  33. public override async Task StopAsync(CancellationToken cancellationToken)
  34. {
  35. _logger.LogInformation("WorkerTwo stopping at: {time}", DateTimeOffset.Now);
  36. await base.StopAsync(cancellationToken);
  37. }
  38. }
  39. }
然后我们在Program类中,将WorkerOne和WorkerTwo服务添加到DI container中:
  1. using Microsoft.Extensions.DependencyInjection;
  2. using Microsoft.Extensions.Hosting;
  3. namespace WorkerService1
  4. {
  5. public class Program
  6. {
  7. public static void Main(string[] args)
  8. {
  9. CreateHostBuilder(args).Build().Run();
  10. }
  11. public static IHostBuilder CreateHostBuilder(string[] args) =>
  12. Host.CreateDefaultBuilder(args)
  13. .ConfigureServices((hostContext, services) =>
  14. {
  15. services.AddHostedService<WorkerOne>();
  16. services.AddHostedService<WorkerTwo>();
  17. //services.AddHostedService<Worker>();
  18. });
  19. }
  20. }
然后在Visual Studio中运行Worker Service,执行结果如下:

Worker Service介绍 - 图8

可以看到WorkerOne和WorkerTwo类都被执行了,并且都输出了日志信息

三、部署为Windows服务运行

Worker Service介绍 - 图9

  • 在program.cs内部,将UseWindowsService()添加到CreateHostBuilder
  1. using Microsoft.Extensions.DependencyInjection;
  2. using Microsoft.Extensions.Hosting;
  3. namespace WorkerService1
  4. {
  5. public class Program
  6. {
  7. public static void Main(string[] args)
  8. {
  9. CreateHostBuilder(args).Build().Run();
  10. }
  11. public static IHostBuilder CreateHostBuilder(string[] args) =>
  12. Host.CreateDefaultBuilder(args)
  13. .UseWindowsService()
  14. .ConfigureServices((hostContext, services) =>
  15. {
  16. services.AddHostedService<Worker>();
  17. });
  18. }
  19. }
注意在非 Windows 平台上调用 UseWindowsService 方法也是不会报错的,非 Windows 平台会忽略此调用。
  • 执行一下命令发布项目
  1. dotnet publish -c Release -o D:\PersonalInfo\windowsservices\WindowsService1\WorkerService1\Release
在Powershell中执行:

Worker Service介绍 - 图10

也可以在Visual Studio中用项目自身的发布向导来将Worker Service项目发布到文件夹”bin\Release\net5.0\publish\”中:

Worker Service介绍 - 图11

  • 默认情况下Worker Service项目会被发布为一个exe文件:

Worker Service介绍 - 图12

  • 使用sc.exe工具来管理服务,输入命令创建为windows服务(Run as administrator)启动Powershell:
  1. sc.exe create NETCoreWorkerService1 binPath=D:\PersonalInfo\windowsservices\WindowsService1\WorkerService1\bin\Release\net5.0\publish\WorkerService1.exe

Worker Service介绍 - 图13

  • 查看服务状态,在powershell中执行(Run as administrator):
  1. sc.exe query NETCoreWorkerService1

Worker Service介绍 - 图14

  • 启动命令,在powershell中执行(Run as administrator):
sc.exe start NETCoreWorkerService1

Worker Service介绍 - 图15

  • 在windows服务列表查看,NETCoreWorkerService1已安装成功:

Worker Service介绍 - 图16

  • 停用 、删除命令:
  1. sc.exe stop NETCoreWorkerService1
  2. sc.exe delete NETCoreWorkerService1

Worker Service介绍 - 图17

四、部署作为Linux守护程序运行

  1. using Microsoft.Extensions.DependencyInjection;
  2. using Microsoft.Extensions.Hosting;
  3. namespace WorkerService1
  4. {
  5. public class Program
  6. {
  7. public static void Main(string[] args)
  8. {
  9. CreateHostBuilder(args).Build().Run();
  10. }
  11. public static IHostBuilder CreateHostBuilder(string[] args) =>
  12. Host.CreateDefaultBuilder(args)
  13. //.UseWindowsService()
  14. .UseSystemd()
  15. .ConfigureServices((hostContext, services) =>
  16. {
  17. services.AddHostedService<Worker>();
  18. });
  19. }
  20. }

在 Windows 平台上调用 UseSystemd 方法也是不会报错的,Windows 平台会忽略此调用。具体如何添加守护进程可以参考https://www.cnblogs.com/qtiger/p/13853828.html

五、参考资料