一文说通Dotnet Core的后台任务
这是一文说通系列的第二篇,里面有些内容会用到第一篇中间件的部分概念。如果需要,可以参看第一篇:一文说通Dotnet Core的中间件
一、前言
后台任务在一些特殊的应用场合,有相当的需求。
比方,我们需要实现一个定时任务、或周期性的任务、或非API输出的业务响应、或不允许并发的业务处理,像提现、支付回调等,都需要用到后台任务。
通常,我们在实现后台任务时,有两种选择:WebAPI和Console。
下面,我们会用实际的代码,来理清这两种工程模式下,后台任务的开发方式。
为了防止不提供原网址的转载,特在这里加上原文链接:https://www.cnblogs.com/tiger-wang/p/13081020.html
二、开发环境&基础工程
这个Demo的开发环境是:Mac + VS Code + Dotnet Core 3.1.2。
$ dotnet --info.NET Core SDK (reflecting any global.json): Version: 3.1.201 Commit: b1768b4ae7
Runtime Environment: OS Name: Mac OS X OS Version: 10.15 OS Platform: Darwin RID: osx.10.15-x64 Base Path: /usr/local/share/dotnet/sdk/3.1.201/
Host (useful for support): Version: 3.1.3 Commit: 4a9f85e9f8
.NET Core SDKs installed: 3.1.201 [/usr/local/share/dotnet/sdk]
.NET Core runtimes installed: Microsoft.AspNetCore.App 3.1.3 [/usr/local/share/dotnet/shared/Microsoft.AspNetCore.App] Microsoft.NETCore.App 3.1.3 [/usr/local/share/dotnet/shared/Microsoft.NETCore.App]
首先,在这个环境下建立工程:
创建Solution
% dotnet new sln -o demoThe template "Solution File" was created successfully.
这次,我们用Webapi创建工程
% cd demo% dotnet new webapi -o webapidemoThe template "ASP.NET Core Web API" was created successfully.
Processing post-creation actions...Running 'dotnet restore' on webapidemo/webapidemo.csproj... Restore completed in 179.13 ms for demo/demo.csproj.
Restore succeeded.% dotnet new console -o consoledemoThe template "Console Application" was created successfully.
Processing post-creation actions...Running 'dotnet restore' on consoledemo/consoledemo.csproj... Determining projects to restore... Restored consoledemo/consoledemo.csproj (in 143 ms).
Restore succeeded.
把工程加到Solution中
% dotnet sln add webapidemo/webapidemo.csproj% dotnet sln add consoledemo/consoledemo.csproj
基础工程搭建完成。
三、在WebAPI下实现一个后台任务
WebAPI下后台任务需要作为托管服务来实现,而托管服务,需要实现IHostedService
接口。
首先,我们需要引入一个库:
% cd webapidemo% dotnet add package Microsoft.Extensions.Hosting
引入后,我们就有了IHostedService
。
下面,我们来做一个IHostedService
的派生托管类:
namespace webapidemo{ public class DemoService : IHostedService { public DemoService() { }
public Task StartAsync(CancellationToken cancellationToken) { throw new NotImplementedException(); }
public Task StopAsync(CancellationToken cancellationToken) { throw new NotImplementedException(); } }}
IHostedService
需要实现两个方法:StartAsync
和StopAsync
。其中:
StartAsync: 用于启动后台任务;
StopAsync:主机Host正常关闭时触发。
如果派生类中有任何非托管资源,那还可以引入IDisposable
,并通过实现Dispose
来清理非托管资源。
这个类生成后,我们将这个类注入到ConfigureServices
中,以使这个类在Startup.Configure
调用之前被调用:
public void ConfigureServices(IServiceCollection services){ services.AddControllers();
services.AddHostedService<DemoService>();}
下面,我们用一个定时器的后台任务,来加深理解:
namespace webapidemo{ public class TimerService : IHostedService, IDisposable { /* 下面这两个参数是演示需要,非必须 */ private readonly ILogger _logger; private int executionCount = 0;
/* 这个是定时器 */ private Timer _timer;
public TimerService(ILogger<TimerService> logger) { _logger = logger; }
public void Dispose() { _timer?.Dispose();
}
private void DoWork(object state) { var count = Interlocked.Increment(ref executionCount);
_logger.LogInformation($"Service proccessing {count}"); }
public Task StartAsync(CancellationToken cancellationToken) { _logger.LogInformation("Service starting");
_timer = new Timer(DoWork, null, TimeSpan.Zero, TimeSpan.FromSeconds(5)); return Task.CompletedTask; }
public Task StopAsync(CancellationToken cancellationToken) { _logger.LogInformation("Service stopping");
_timer?.Change(Timeout.Infinite, 0); return Task.CompletedTask; } }}
注入到ConfigureServices
中:
public void ConfigureServices(IServiceCollection services){ services.AddControllers();
services.AddHostedService<TimerService>();}
就OK了。代码比较简单,就不解释了。
四、WebAPI后台任务的依赖注入变形
上一节的示例,是一个简单的形态。
下面,我们按照标准的依赖注入,实现一下这个定时器。
依赖注入的简单样式,请参见一文说通Dotnet Core的中间件。
首先,我们创建一个接口IWorkService
:
namespace webapidemo{ public interface IWorkService { Task DoWork(); }}
再根据IWorkService
,建立一个实体类:
namespace webapidemo{ public class WorkService : IWorkService { private readonly ILogger _logger; private Timer _timer; private int executionCount = 0;
public WorkService(ILogger<WorkService> logger) { _logger = logger; }
public async Task DoWork() { var count = Interlocked.Increment(ref executionCount);
_logger.LogInformation($"Service proccessing {count}"); } }}
这样就建好了依赖的全部内容。
下面,创建托管类:
namespace webapidemo{ public class HostedService : IHostedService, IDisposable { private readonly ILogger<HostedService> _logger; public IServiceProvider Services { get; } private Timer _timer;
public HostedService(IServiceProvider services, ILogger<HostedService> logger) { Services = services; _logger = logger; }
public void Dispose() { _timer?.Dispose(); }
private void DoWork(object state) { _logger.LogInformation("Service working");
using (var scope = Services.CreateScope()) { var scopedProcessingService = scope.ServiceProvider .GetRequiredService<IWorkService>();
scopedProcessingService.DoWork().GetAwaiter().GetResult(); } }
public Task StartAsync(CancellationToken cancellationToken) { _logger.LogInformation("Service starting");
_timer = new Timer(DoWork, null, TimeSpan.Zero, TimeSpan.FromSeconds(5)); return Task.CompletedTask; }
public Task StopAsync(CancellationToken cancellationToken) { _logger.LogInformation("Service stopping");
_timer?.Change(Timeout.Infinite, 0); return Task.CompletedTask; } }}
把托管类注入到ConfigureServices
中:
public void ConfigureServices(IServiceCollection services){ services.AddControllers();
services.AddHostedService<HostedService>(); services.AddSingleton<IWorkService, WorkService>();}
这样就完成了。
这种模式下,可以根据注入的内容切换应用的执行内容。不过,这种模式需要注意services.AddSingleton
、services.AddScoped
和services.AddTransient
的区别。
五、Console下的后台任务
Console应用本身就是后台运行,所以区别于WebAPI,它不需要托管运行,也不需要Microsoft.Extensions.Hosting
库。
我们要做的,就是让程序运行,就OK。
下面是一个简单的Console模板:
namespace consoledemo{ class Program { private static AutoResetEvent _exitEvent;
static async Task Main(string[] args) { /* 确保程序只有一个实例在运行 */ bool isRuned; Mutex mutex = new Mutex(true, "OnlyRunOneInstance", out isRuned); if (!isRuned) return;
await DoWork();
/* 后台等待 */ _exitEvent = new AutoResetEvent(false); _exitEvent.WaitOne(); }
private static async Task DoWork() { throw new NotImplementedException(); } }}
这个模板有两个关键的内容:
单实例运行:通常后台任务,只需要有一个实例运行。所以,第一个小段,是解决单实例运行的。多次启动时,除了第一个实例外,其它的实例会自动退出;
后台等待:看过很多人写的,在这儿做后台等待时,用了一个无限的循环。类似于下面的:
while(true){ Thread.Sleep(1000);}
这种方式也没什么太大的问题。不过,这段代码总是要消耗CPU的计算量,虽然很少,但做为后台任务,或者说Service,毕竟是一种消耗,而且看着不够高大上。
当然如果我们需要中断,我们也可以把这个模板改成这样:
namespace consoledemo{ class Program { private static AutoResetEvent _exitEvent;
static async Task Main(string[] args) { bool isRuned; Mutex mutex = new Mutex(true, "OnlyRunOneInstance", out isRuned); if (!isRuned) return;
_exitEvent = new AutoResetEvent(false); await DoWork(_exitEvent); _exitEvent.WaitOne(); }
private static async Task DoWork(AutoResetEvent _exitEvent) { /* Your Code Here */
_exitEvent.Set(); } }}
这样就可以根据需要,来实现中断程序并退出。
六、Console应用的其它运行方式
上一节介绍的Console,其实是一个应用程序。
在实际应用中,Console程序跑在Linux服务器上,我们可能会有一些其它的要求:
定时运行
Linux上有一个Service,叫cron,是一个用来定时执行程序的服务。
这个服务的设定,需要另一个命令:crontab,位置在/usr/bin
下。
具体命令格式这儿不做解释,网上随便查。
运行到后台
命令后边加个&
字符即可:
$ ./command &
运行为Service
需要持续运行的应用,如果以Console的形态存在,则设置为Service是最好的方式。
Linux下,设置一个应用为Service很简单,就这么简单三步:
第一步:在/etc/systemd/system
下面,创建一个service文件,例如command.service
:
[Unit]# Service的描述,随便写Description=Command
[Service]RestartSec=2sType=simple# 执行应用的默认用户。应用如果没有特殊要求,最好别用root运行User=your_user_nameGroup=your_group_name# 应用的目录,绝对路径WorkingDirectory=your_app_folder# 应用的启动路径ExecStart=your_app_folder/your_appRestart=always
[Install]WantedBy=multi-user.target
差不多就这么个格式。参数的详细说明可以去网上查,实际除了设置,就是运行了一个脚本。
第二步:把这个command.service
加上运行权限:
# chmod +x ./command.service
第三步:注册为Service:
# systemctl enable command.service
完成。
为了配合应用,还需要记住两个命令:启动和关闭Service
# #启动Service# systemctl start command.service# #关闭Service# systemctl stop command.service
七、写在后边的话
今天这个文章,是因为前两天,一个兄弟跑过来问我关于数据总线的实现方式,而想到的一个点。
很多时候,大家在写代码的时候,会有一种固有的思想:写WebAPI,就想在这个框架中把所有的内容都实现了。这其实不算是一个很好的想法。WebAPI,在业务层面,就应该只是实现简单的处理请求,返回结果的工作,而后台任务跟这个内容截然不同,通常它只做处理,不做返回 --- 事实上也不太好返回,要么客户端等待时间太长,要么客户端已经断掉了。换句话说,用WebAPI实现总线,绝不是一个好的方式。
不过,Console运行为Service,倒是一个总线应用的绝好方式。如果需要按序执行,可以配合MQ服务器,例如RabbitMQ,来实现消息的按序处理。
再说代码。很多需求,本来可以用很简单的方式实现。模式这个东西,用来面试,用来讲课,都是很好的内容,但实际开发中,如果有更简单更有效的方式,用起来!Coding的工作是实现,而不是秀技术。当然,能否找到简单有效的方式,这个可能跟实际的技术面有关系。但这并不是一个不能跨越的坎。
多看,多想,每天成长一点点!
今天的代码,在:https://github.com/humornif/Demo-Code/tree/master/0012/demo
(全文完)
本文版权归作者所有,转载请保留此声明和原文链接 |