一、前言
后台任务在一些特殊的应用场所,有相当的需求。
比如,我们需求完成一个定时任务、或周期性的任务、或非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 demo
The template “Solution File” was created successfully.
这次,我们用Webapi创立工程
% cd demo
% dotnet new webapi -o webapidemo
The 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 consoledemo
The 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 _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=2s
Type=simple
执行应用的默许用户。应用假如没有特殊请求,最好别用root运转
User=your_user_name
Group=your_group_name
应用的目录,绝对途径
WorkingDirectory=your_app_folder
应用的启动途径
ExecStart=your_app_folder/your_app
Restart=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,来完成音讯的按序处置。