如何在跨平台的环境中创建可以跨平台的后台服务,它就是 Worker Service。
一、简介
最近,有一个项目要使用 Windows 服务,来做为一个软件项目的载体。我想了想,都已经到了跨平台的时代了,会不会有替换 Windows 服务的技术出现呢?于是,在网络上疯狂的搜索了一番,真实皇天不负苦心人,找到了一个答案,那就是 Worker Service。听说在 NET Core 3.0 的时代就新增了 Worker Service 的新项目模板,可以编写长时间运行的后台服务,并且能轻松的部署成 windows 服务或 linux 守护程序。如果安装的 vs2019 是中文版本,Worker Service 的项目名称就变成了辅助角色服务。我也没有学习的太深入,就是把自己的使用过程展示出来,本文将会演示如何创建一个 Worker Service 项目,并且部署为 windows 服务或 linux 守护程序运行;废话不多说,开始。
二、开始实战
1、开始创建 worker service 项目
1.1、创建新项目——》选择 Worker Service(辅助角色服务) 
1.2、给项目取一个名称:DemoWorkerService

项目创建成功之后,您会看到创建了两个类:Program.cs 和 Worker.cs。

1.3、Program.cs 程序入口的类型。
1 using System;
2 using System.Collections.Generic;
3 using System.Linq;
4 using System.Threading.Tasks;
5 using Microsoft.Extensions.DependencyInjection;
6 using Microsoft.Extensions.Hosting;
7
8 namespace DemoWorkerService
9 {
10 public class Program
11 {
12 public static void Main(string[] args)
13 {
14 CreateHostBuilder(args).Build().Run();
15 }
16
17 public static IHostBuilder CreateHostBuilder(string[] args) =>
18 Host.CreateDefaultBuilder(args)
19 .ConfigureServices((hostContext, services) =>
20 {
21 services.AddHostedService<Worker>();
22 });
23 }
24 }
Program 类跟 ASP.NET Core Web 应用程序非常类似,不同之处没有了 startup 类,并且把 worker 服务添加到 IOC 容器中。
1.4、Worker.cs 具体的承担任务的工作类型。
1 using System;
2 using System.Collections.Generic;
3 using System.Linq;
4 using System.Threading;
5 using System.Threading.Tasks;
6 using Microsoft.Extensions.Hosting;
7 using Microsoft.Extensions.Logging;
8
9 namespace DemoWorkerService
10 {
11 public class Worker : BackgroundService
12 {
13 private readonly ILogger<Worker> _logger;
14
15 public Worker(ILogger<Worker> logger)
16 {
17 _logger = logger;
18 }
19
20 protected override async Task ExecuteAsync(CancellationToken stoppingToken)
21 {
22 while (!stoppingToken.IsCancellationRequested)
23 {
24 _logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now);
25 await Task.Delay(1000, stoppingToken);
26 }
27 }
28 }
29 }
Worker.cs 只是一个简单的类,它继承自 BackgroundService ,而后者又实现 IHostedService 接口。
2、依赖注入(DI)
我们可以在 Program 类中的 ConfigureServices 方法中,配置 Worker 类要用到的类型,这叫做“构造函数注入”,假如我们现在有 IContainer 接口和 MyContainer 类,它们是一组类型:
1 public interface IContainer
2 {
3 string ContainerName
4 {
5 get;
6 set;
7 }
8 }
9
10 public class MyContainer : IContainer
11 {
12 protected string containerName = "Custom my container";
13
14 public string ContainerName
15 {
16 get
17 {
18 return containerName;
19 }
20 set
21 {
22 containerName = value;
23 }
24 }
25 }
2.1、我们可以在 Program 类中的 ConfigureServices 方法中,使用 services 参数(该参数是 IServiceCollection 接口的对象,它就是 IOC 容器)来配置 IContainer 接口和 MyContainer 类的对应关系:
1 using System;
2 using System.Collections.Generic;
3 using System.Linq;
4 using System.Threading.Tasks;
5 using DemoWorkerService.Model;
6 using Microsoft.Extensions.DependencyInjection;
7 using Microsoft.Extensions.Hosting;
8
9 namespace DemoWorkerService
10 {
11 public class Program
12 {
13 public static void Main(string[] args)
14 {
15 CreateHostBuilder(args).Build().Run();
16 }
17
18 public static IHostBuilder CreateHostBuilder(string[] args) =>
19 Host.CreateDefaultBuilder(args)
20 .ConfigureServices((hostContext, services) =>
21 {
22 //配置IContainer接口和MyContainer类的依赖注入关系
23 services.AddSingleton<IContainer,MyContainer>();
24 services.AddHostedService<Worker>();
25 });
26 }
27 }
2.2、然后在 Worker 类的构造函数中,通过构造函数注入,来使用其类型。Worker Service 就会使用DI(依赖注入)自动注入 IContainer 类型的参数:
1 using System;
2 using System.Collections.Generic;
3 using System.Linq;
4 using System.Threading;
5 using System.Threading.Tasks;
6 using DemoWorkerService.Model;
7 using Microsoft.Extensions.Hosting;
8 using Microsoft.Extensions.Logging;
9
10 namespace DemoWorkerService
11 {
12 public class Worker : BackgroundService
13 {
14 private readonly ILogger<Worker> _logger;
15 private readonly IContainer _container;
16
17 //Worker Service会自动依赖注入Worker构造函数的IContainer container参数
18 public Worker(ILogger<Worker> logger, IContainer container)
19 {
20 _logger = logger;
21 _container = container;
22 }
23
24 protected override async Task ExecuteAsync(CancellationToken stoppingToken)
25 {
26 while (!stoppingToken.IsCancellationRequested)
27 {
28 _logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now);
29 await Task.Delay(1000, stoppingToken);
30 }
31 }
32 }
33 }
· 2.3、注意上面 Worker 类的构造函数中,使用了 .NET Core 自带的日志组件接口对象 ILogger,它也是通过构造函数注入到 Worker 类型内部来使用的,和 ASP.NET Core 中 Controller 的构造函数注入类似(关于 ILogger,可以查看这里了解),我们也可以不用日志组件,给 Worker 类定义无参的默认构造函数。
1 using System;
2 using System.Collections.Generic;
3 using System.Linq;
4 using System.Threading;
5 using System.Threading.Tasks;
6 using Microsoft.Extensions.Hosting;
7
8 namespace DemoWorkerService
9 {
10 public class Worker : BackgroundService
11 {
12 public Worker()
13 {
14 }
15
16 protected override async Task ExecuteAsync(CancellationToken stoppingToken)
17 {
18 while (!stoppingToken.IsCancellationRequested)
19 {
20 await Task.Delay(1000, stoppingToken);
21 }
22 }
23 }
24 }
3、重写 BackgroundService 类的 StartAsync()、ExecuteAsync()、StopAsync() 方法
我们可以通过 override ExecuteAsync 方法来完成自己要做的事情,该方法实际上属于 BackgroundService 类,我们只是在 Worker 类中重写(override)了它而已。在这个方法中就是我们要处理的逻辑,原则上来说这个逻辑应该是在一个死循环中,并且通过 ExecuteAsync 方法传入的 CancellationToken 参数对象,来判断是否应该结束循环。例如:如果 Windows 服务被停止,那么参数中 CancellationToken 类的 IsCancellationRequested 属性会返回 true,那么我们应该停止 ExecuteAsync 方法中的循环,来结束整个服务过程的执行:
1 //重写BackgroundService.ExecuteAsync方法,封装windows服务或linux守护程序中的处理逻辑
2 protected override async Task ExecuteAsync(CancellationToken stoppingToken)
3 {
4 //如果服务被停止,那么下面的IsCancellationRequested会返回true,我们就应该结束循环
5 while (!stoppingToken.IsCancellationRequested)
6 {
7 //模拟服务中的处理逻辑,这里我们仅输出一条日志,并且等待1秒钟时间
8 _logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now);
9 await Task.Delay(1000, stoppingToken);
10 }
11 }
此外,我们也可以在 Worker 类中重写 BackgroundService.StartAsync() 方法和 BackgroundService.StopAsync() 方法(注意重写时,不要忘记在 Worker 类中调用 base.StartAsync() 和 base.StopAsync() 方法,因为 BackgroundService 类的 StartAsync() 方法和 StopAsyn() 方法会执行一些 Worker Service 的核心代码)。在开始和结束 Worker Service 服务(例如,开始和停止 windows 服务)的时候,来执行一些处理逻辑,本例中我们分别输出了一条日志:
1 using System;
2 using System.Collections.Generic;
3 using System.Linq;
4 using System.Threading;
5 using System.Threading.Tasks;
6 using Microsoft.Extensions.Hosting;
7 using Microsoft.Extensions.Logging;
8
9 namespace DemoWorkerService
10 {
11 public class Worker : BackgroundService
12 {
13 private readonly ILogger<Worker> _logger;
14
15 public Worker(ILogger<Worker> logger)
16 {
17 _logger = logger;
18 }
19
20 //重写BackgroundService.StartAsync方法,在开始服务的时候,执行一些处理逻辑,这里我们仅输出一条日志
21 public override async Task StartAsync(CancellationToken cancellationToken)
22 {
23 _logger.LogInformation("Worker starting at: {time}", DateTimeOffset.Now);
24
25 await base.StartAsync(cancellationToken);
26 }
27
28 //重写BackgroundService.ExecuteAsync方法,封装windows服务或linux守护程序中的处理逻辑
29 protected override async Task ExecuteAsync(CancellationToken stoppingToken)
30 {
31 //如果服务被停止,那么下面的IsCancellationRequested会返回true,我们就应该结束循环
32 while (!stoppingToken.IsCancellationRequested)
33 {
34 //模拟服务中的处理逻辑,这里我们仅输出一条日志,并且等待1秒钟时间
35 _logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now);
36 await Task.Delay(1000, stoppingToken);
37 }
38 }
39
40 //重写BackgroundService.StopAsync方法,在结束服务的时候,执行一些处理逻辑,这里我们仅输出一条日志
41 public override async Task StopAsync(CancellationToken cancellationToken)
42 {
43 _logger.LogInformation("Worker stopping at: {time}", DateTimeOffset.Now);
44
45 await base.StopAsync(cancellationToken);
46 }
47 }
48 }
由于 BackgroundService 类的 StartAsync()、ExecuteAsync()、StopAsync() 方法返回的都是 Task 类型,所以如同上面代码所示,我们可以使用 async 和 await 关键字将它们重写为异步函数,来提高程序的可用性。现在我们在 Visual Studio 中直接运行上面的代码,结果如下所示,每隔1秒,循环打印运行的时间:

其中,我用三个红色框,将 Worker 类中重写 StartAsync()、ExecuteAsync()、StopAsync() 方法的输出结果标识了出来,在 Visual Studio 中运行 Worker Service 项目时,可以在启动的控制台中使用快捷键 "Ctrl+C" 来停止 Worker Service 的运行(相当于停止 windows 服务或 linux 守护程序),所以这样我们可以在上面的结果中看到 StartAsync、ExecuteAsync、StopAsync 三个方法都被执行了,并且都输出了日志。
其实我们可以看到 Worker Service 项目从本质上来说就是一个控制台项目,只不过当它被部署为 windows 服务或 linux 守护程序后,不会显示控制台窗口。

所以实际上在 Visual Studio 中进行调试的时候,完全可以用 Console.WriteLine() 等控制台方法来替代 ILogger 接口的日志输出方法:
1 using System;
2 using System.Collections.Generic;
3 using System.Linq;
4 using System.Threading;
5 using System.Threading.Tasks;
6 using Microsoft.Extensions.Hosting;
7 using Microsoft.Extensions.Logging;
8
9 namespace DemoWorkerService
10 {
11 public class Worker : BackgroundService
12 {
13 public Worker()
14 {
15 }
16
17 //重写BackgroundService.StartAsync方法,在开始服务的时候,执行一些处理逻辑,这里我们仅输出一条日志
18 public override async Task StartAsync(CancellationToken cancellationToken)
19 {
20 Console.WriteLine("Worker starting at: {0}", DateTimeOffset.Now);
21
22 await base.StartAsync(cancellationToken);
23 }
24
25 //重写BackgroundService.ExecuteAsync方法,封装windows服务或linux守护程序中的处理逻辑
26 protected override async Task ExecuteAsync(CancellationToken stoppingToken)
27 {
28 //如果服务被停止,那么下面的IsCancellationRequested会返回true,我们就应该结束循环
29 while (!stoppingToken.IsCancellationRequested)
30 {
31 //模拟服务中的处理逻辑,这里我们仅输出一条日志,并且等待1秒钟时间
32 Console.WriteLine("Worker running at: {0}", DateTimeOffset.Now);
33 await Task.Delay(1000, stoppingToken);
34 }
35 }
36
37 //重写BackgroundService.StopAsync方法,在结束服务的时候,执行一些处理逻辑,这里我们仅输出一条日志
38 public override async Task StopAsync(CancellationToken cancellationToken)
39 {
40 Console.WriteLine("Worker stopping at: {0}", DateTimeOffset.Now);
41
42 await base.StopAsync(cancellationToken);
43 }
44 }
45 }
其效果和 ILogger 接口的日志输出方法类似:

不过由于 ILogger 接口的日志输出方法,也可以输出信息到控制台上,所以我还是更推荐使用 ILogger 接口来输出调试信息,毕竟它更适合做日志记录。
4、不要让线程阻塞 worker 类中重写的 StartAsync()、ExecuteAsync()、StopAsync() 方法
注意:不要让你的代码阻塞 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
9 await Task.CompletedTask;
10 }
我们将 ExecuteAsync() 方法中的异步等待方法 Task.Delay,改为了同步等待方法 Thread.Sleep(关于 Thread.Sleep 和 Task.Delay 有什么不同,请查看这里)。由于 Thread.Sleep 方法是将执行线程通过阻塞的方式来进行等待,所以现在调用 ExecuteAsync 方法的线程会一直执行 ExecuteAsync 方法中的循环,被不停地被阻塞,除非 ExecuteAsync 方法中的循环结束,那么调用 ExecuteAsync 方法的线程会被一直卡在 ExecuteAsync 方法中。现在我们在 Visual Studio 中运行 Worker Service,执行结果如下:

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

可以看到这次,当我们在控制台中使用快捷键 "Ctrl+C" 试图停止 Worker Service 后(上图红色框中输出的日志),ExecuteAsync() 方法就立即停止运行了,所以这里再次强调千万不要去阻塞调用 ExecuteAsync 方法的线程!
另外上面代码中,我们在worker类重写的ExecuteAsync方法中放了一个 finally 代码块,这个代码块可以用来执行一些Worker Service服务停止后(例如停止 windows服务或linux守护程序 时)的一些收尾代码逻辑(例如关闭数据库连接、释放资源等),我更倾向于使用 ExecuteAsync() 方法中的 finally 代码块来做 Worker Service 的收尾工作,而不是在 worker 类重写的 StopAsync() 方法中来做收尾工作(从 BackgroundService 的源代码,我们可以看出 worker 类的 StopAsync() 方法是有可能比ExecuteAsync() 方法先完成的,所以 Worker Service 的收尾工作应该放到 ExecuteAsync() 方法中的 finally 代码块),因为 ExecuteAsync() 方法中的 finally 代码块,肯定是在RunTaskOne、RunTaskTwo、RunTaskThree 方法返回的三个 Task 对象执行完毕后才执行的。
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.Collections.Generic;
5 using System.Text;
6 using System.Threading;
7 using System.Threading.Tasks;
8
9 namespace DemoWorkerService
10 {
11 public class WorkerOne : BackgroundService
12 {
13 private readonly ILogger<Worker> _logger;
14
15 public WorkerOne(ILogger<Worker> logger)
16 {
17 _logger = logger;
18 }
19
20 //重写BackgroundService.StartAsync方法,在开始服务的时候,执行一些处理逻辑,这里我们仅输出一条日志
21 public override async Task StartAsync(CancellationToken cancellationToken)
22 {
23 _logger.LogInformation("WorkerOne starting at: {time}", DateTimeOffset.Now);
24
25 await base.StartAsync(cancellationToken);
26 }
27
28 //重写BackgroundService.ExecuteAsync方法,封装windows服务或linux守护程序中的处理逻辑
29 protected override async Task ExecuteAsync(CancellationToken stoppingToken)
30 {
31 //如果服务被停止,那么下面的IsCancellationRequested会返回true,我们就应该结束循环
32 while (!stoppingToken.IsCancellationRequested)
33 {
34 //模拟服务中的处理逻辑,这里我们仅输出一条日志,并且等待1秒钟时间
35 _logger.LogInformation("WorkerOne running at: {time}", DateTimeOffset.Now);
36 await Task.Delay(1000, stoppingToken);
37 }
38 }
39
40 //重写BackgroundService.StopAsync方法,在结束服务的时候,执行一些处理逻辑,这里我们仅输出一条日志
41 public override async Task StopAsync(CancellationToken cancellationToken)
42 {
43 _logger.LogInformation("WorkerOne stopping at: {time}", DateTimeOffset.Now);
44
45 await base.StopAsync(cancellationToken);
46 }
47 }
48 }
接着我们定义第二个 Worker 类 WorkerTwo:
1 using Microsoft.Extensions.Hosting;
2 using Microsoft.Extensions.Logging;
3 using System;
4 using System.Collections.Generic;
5 using System.Text;
6 using System.Threading;
7 using System.Threading.Tasks;
8
9 namespace DemoWorkerService
10 {
11 public class WorkerTwo : BackgroundService
12 {
13 private readonly ILogger<WorkerTwo> _logger;
14
15 public WorkerTwo(ILogger<WorkerTwo> logger)
16 {
17 _logger = logger;
18 }
19
20 //重写BackgroundService.StartAsync方法,在开始服务的时候,执行一些处理逻辑,这里我们仅输出一条日志
21 public override async Task StartAsync(CancellationToken cancellationToken)
22 {
23 _logger.LogInformation("WorkerTwo starting at: {time}", DateTimeOffset.Now);
24
25 await base.StartAsync(cancellationToken);
26 }
27
28 //重写BackgroundService.ExecuteAsync方法,封装windows服务或linux守护程序中的处理逻辑
29 protected override async Task ExecuteAsync(CancellationToken stoppingToken)
30 {
31 //如果服务被停止,那么下面的IsCancellationRequested会返回true,我们就应该结束循环
32 while (!stoppingToken.IsCancellationRequested)
33 {
34 //模拟服务中的处理逻辑,这里我们仅输出一条日志,并且等待1秒钟时间
35 _logger.LogInformation("WorkerTwo running at: {time}", DateTimeOffset.Now);
36 await Task.Delay(1000, stoppingToken);
37 }
38 }
39
40 //重写BackgroundService.StopAsync方法,在结束服务的时候,执行一些处理逻辑,这里我们仅输出一条日志
41 public override async Task StopAsync(CancellationToken cancellationToken)
42 {
43 _logger.LogInformation("WorkerTwo stopping at: {time}", DateTimeOffset.Now);
44
45 await base.StopAsync(cancellationToken);
46 }
47 }
48 }
然后我们在 Program 类中,将 WorkerOne 和 WorkerTwo 服务添加到 DI container 中:
1 using System;
2 using System.Collections.Generic;
3 using System.Linq;
4 using System.Threading;
5 using System.Threading.Tasks;
6 using Microsoft.Extensions.DependencyInjection;
7 using Microsoft.Extensions.Hosting;
8
9 namespace DemoWorkerService
10 {
11 public class Program
12 {
13 public static void Main(string[] args)
14 {
15 CreateHostBuilder(args).Build().Run();
16 }
17
18 public static IHostBuilder CreateHostBuilder(string[] args) =>
19 Host.CreateDefaultBuilder(args)
20 .ConfigureServices((hostContext, services) =>
21 {
22 services.AddHostedService<WorkerOne>();
23 services.AddHostedService<WorkerTwo>();
24 });
25 }
26 }
27
28
然后在Visual Studio中运行Worker Service,执行结果如下:

在控制台中使用快捷键 "Ctrl+C" 来停止 Worker Service 的运行后,我用三个红色框,将 WorkerOne 和 WorkerTwo 类中重写 StartAsync、ExecuteAsync、StopAsync 方法的输出结果标识了出来,可以看到 WorkerOne 和 WorkerTwo 类都被执行了,并且都输出了日志信息。
6、部署为Windows服务运行
6.1、在项目中添加nuget包:Microsoft.Extensions.Hosting.WindowsServices

6.2、然后在program.cs内部,将UseWindowsService()添加到CreateHostBuilder
1 public static IHostBuilder CreateHostBuilder(string[] args) =>
2 Host.CreateDefaultBuilder(args)
3 .UseWindowsService();
4 {
5 services.AddHostedService<Worker>();
6 });
注意,在非 Windows 平台上调用 UseWindowsService 方法也是不会报错的,非 Windows 平台会忽略此调用。
6.3、执行一下命令发布项目
dotnet publish -c Release -o C:\WorkerPub
在CMD中执行:

当然,也可以在Visual Studio中用项目自身的发布向导来将Worker Service项目发布到文件夹"C:\WorkerPub"中:


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

6.4、然后使用 sc.exe 工具来管理服务,输入一下命令创建为 windows 服务,这里我们将 DemoWorkerService.exe 创建为了名为NETCoreDemoWorkerService 的 windows 服务(如果服务名称或者文件路径有空格,必须用引号括起来,如:"My Best Server"):
sc.exe create NETCoreDemoWorkerService binPath=C:\WorkerPub\DemoWorkerService.exe
以上 binPath=C:\WorkerPub\DemoWorkerService.exe 是绝对地址的方式,如果是相对地址可以写成 binPath=./WorkerPub\DemoWorkerService.exe
sc.exe create NETCoreDemoWorkerService binPath=DemoWorkerService.exe
在CMD中执行,注意这里要用管理员模式(Run as administrator)启动CMD:

查看服务状态使用一下命令:
sc.exe query NETCoreDemoWorkerService
在CMD中执行(Run as administrator):

启动命令(这个命令是在Net Core3.1中,以后可能有变):
sc.exe start NETCoreDemoWorkerService
在 CMD 中执行(Run as administrator):

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

停用 、删除命令:
sc.exe stop NETCoreDemoWorkerService
sc.exe delete NETCoreDemoWorkerService
在CMD中执行(Run as administrator):

7、部署作为Linux守护程序运行
部署 linux 守护程序也是很方便的执行一下两个步骤即可:
添加 Microsoft.Extensions.Hosting.Systemd NuGet包到项目中,并告诉你的新Worker,其生命周期由systemd管理!
将 UseSystemd() 添加到主机构建器中
1 public static IHostBuilder CreateHostBuilder(string[] args) =>
2 Host.CreateDefaultBuilder(args)
3 .UseSystemd();
4 {
5 services.AddHostedService<Worker>();
6 });
同样,在 Windows 平台上调用 UseSystemd 方法也是不会报错的,Windows 平台会忽略此调用。
三、总结
个人感觉 Worker Service 比 Windows 服务好用多了,无论是安装、配置和卸载都很方便,如果大家使用过 Windows服务,就知道它的过程还是很多的,又要添加安装程序等等。现在有了这个项目,以后在开发后台服务就是容易多了。好了,就是这么多。继续努力吧。
如何在跨平台的环境中创建可以跨平台的后台服务,它就是 Worker Service。的更多相关文章
- 她娇羞道“不用这样细致认真的说啊~~”———详细图解在Linux环境中创建运行C程序
她娇羞说,不用这样细致认真的说啊———详细图解在Linux环境中创建运行C程序“不,这是对学习的负责”我认真说到 叮叮叮,停车,让我们看看如何在Linux虚拟机环境中,创建运行C程序 详细图解在Lin ...
- neutron中创建子网时禁用dhcp服务的问题
在neutron中创建provider网络时,可以指定是否禁用dhcp.若禁用,就可以使用物理网络中的dhcp服务.若使用物理网络的dhcp,就要禁用子网中提供的.如图
- .NET Core 中的通用主机和后台服务
简介 我们在做项目的时候, 往往要处理一些后台的任务. 一般是两种, 一种是不停的运行,比如消息队列的消费者.另一种是定时任务. 在.NET Framework + Windows环境里, 我们一般会 ...
- OpenShift 4.3环境中创建基于Go的Operator
详细步骤可以参考官方文档 https://docs.openshift.com/container-platform/4.3/operators/operator_sdk/osdk-getting-s ...
- lanmp环境中创建个软连接
进入default,创建关于框架的链接 然后编辑配置文件 这样就可以在线上访问了.
- linux环境中通过useradd命令,创建用户的时候指定用户的base-dir
需求说明: 今天一个同事,问了一个这样的问题,在linux环境中,创建用户的时候,默认的是在/home目录下创建一个与用户名相同的家目录, 如何能够将这个/home更换成一个其他的,比如/opt/ap ...
- 《Linux命令行与shell脚本编程大全》第十八章 图形化桌面环境中的脚本编程
18.1 创建文本菜单 直接上例子吧: 1 #!/bin/bash 2 function menu 3 { 4 clear 5 echo 6 ...
- 在kubernetes集群中创建redis主从多实例
分类 > 正文 在kubernetes集群中创建redis主从多实例 redis-slave镜像制作 redis-master镜像制作 创建kube的配置文件yaml 继续使用上次实验环境 ht ...
- 在 k8s 以外的分布式环境中使用 Dapr
在Dapr 文档和实践案例中多是推荐采用k8s, 其实我目前也是在k8s 上操作的,有公有云TKE,AKS,还有私有云的Rancher ,它并没有传闻中的那么难,而且我认为它非常容易上手.不过,我还是 ...
- 企业运维 | MySQL关系型数据库在Docker与Kubernetes容器环境中快速搭建部署主从实践
[点击 关注「 WeiyiGeek」公众号 ] 设为「️ 星标」每天带你玩转网络安全运维.应用开发.物联网IOT学习! 希望各位看友[关注.点赞.评论.收藏.投币],助力每一个梦想. 本章目录 目录 ...
随机推荐
- dicker 常用命令(简洁版)
- [vue]精宏技术部试用期学习笔记 III
精宏技术部试用期学习笔记(vue) 父子通信 什么是通信 / 为什么要通信 通信即在不同组件之间传输数据 当在 复用组件 时,需要传递不同数据达成不同的表现效果 能够根据其他组件的行动,响应式 的做出 ...
- SpringBoot 项目优雅实现读写分离
一.读写分离介绍 当使用Spring Boot开发数据库应用时,读写分离是一种常见的优化策略.读写分离将读操作和写操作分别分配给不同的数据库实例,以提高系统的吞吐量和性能. 读写分离实现主要是通过动态 ...
- SpringCloud全链路灰色发布具体实现!
灰度发布(Gray Release,也称为灰度发布或金丝雀发布)是指在软件或服务发布过程中,将新版本的功能或服务以较小的比例引入到生产环境中,仅向部分用户或节点提供新功能的一种发布策略. 在传统的全量 ...
- C/C++ 运用VMI接口查询系统信息
Windows Management Instrumentation(WMI)是一种用于管理和监视Windows操作系统的框架.它为开发人员.系统管理员和自动化工具提供了一种标准的接口,通过这个接口, ...
- Jenkins中HTML报告无法正常显示问题解决
自动化结果生成了HTML报告,但是在Jenkins中打开报告却显示空白,打开控制台,可以看到该报错 参考https://www.jenkins.io/doc/book/security/configu ...
- MCU看门狗使用注意事项
前言 最近因为项目产品硬件设计有问题,导致设计的一款产品把硬件电源开关以及硬件系统复位功能去掉了.更严重的是,这产品已经开始生产了,硬件已经无法修改,所以软件必须上看门狗,否则设备死机或是异常后就只能 ...
- [ICPC2015WF] Tours
题目描述 The Arca Carania Mountain national park is opening up for tourist traffic. The national park ha ...
- js上传多个文件到asp.net core,并实时转存到阿里云oss
有时候,为了追求便利性,我们可能会让前端直接将文件上传到阿里云OSS,然后将URL提交给ASP.NET.然而,这种做法意味着前端需要拥有OSS的访问密钥,而将密钥存放在前端,无疑增加了被破解的风险.因 ...
- LeetCode227:基本计算器|| (栈、模拟)
解题思路:两个双端队列模拟,一个存放操作数 a,另一个存放操作符 op,如果找到另一个操作数b,判断操作队列队尾是否是*/,是的话执行 a(*or/)b.遍历完字符串,如果操作符队列非空,说明还有+- ...