这是一文说通系列的第二篇,里面有些内容会用到第一篇中间件的部分概念。如果需要,可以参看第一篇:一文说通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?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(); ????????}
????????StopAsyncnew?NotImplementedException(); ????????} ????} }
IHostedService 需要实现两个方法:StartAsync 和StopAsync 。其中:
StartAsync: 用于启动后台任务;
StopAsync:主机Host正常关闭时触发。
?
如果派生类中有任何非托管资源,那还可以引入IDisposable ,并通过实现Dispose 来清理非托管资源。
?
这个类生成后,我们将这个类注入到ConfigureServices 中,以使这个类在Startup.Configure 调用之前被调用:
public?void?ConfigureServices(IServiceCollection?services) { ????services.AddControllers();
????services.AddHostedService<DemoService>(); }
下面,我们用一个定时器的后台任务,来加深理解:
TimerService?:?IHostedService,?IDisposable ????{ ?????????? ????????private?readonly?ILogger?_logger; ????????private?int?executionCount?=?0;
?????????? ????????private?Timer?_timer;
????????TimerService(ILogger<TimerService>?logger) ????????{ ????????????_logger?=?logger; ????????}
????????Dispose() ????????{ ????????????_timer?.Dispose();
????????}
????????DoWork(object?state) ????????{ ????????????var?count?=?Interlocked.Increment(ref?executionCount);
????????????_logger.LogInformation($"Service?proccessing?{count}"); ????????}
????????(CancellationToken?cancellationToken) ????????{ ????????????_logger.LogInformation("Service?starting");
????????????_timer?=?new?Timer(DoWork,?null,?TimeSpan.Zero,?TimeSpan.FromSeconds(5)); ????????????return?Task.CompletedTask; ????????}
????????"Service?stopping");
????????????_timer?.Change(Timeout.Infinite,?0); ????????????return?Task.CompletedTask; ????????} ????} }
注入到ConfigureServices 中:
(IServiceCollection?services) { ????services.AddControllers();
????services.AddHostedService<TimerService>(); }
就OK了。代码比较简单,就不解释了。
四、WebAPI后台任务的依赖注入变形
上一节的示例,是一个简单的形态。
下面,我们按照标准的依赖注入,实现一下这个定时器。
?
依赖注入的简单样式,请参见一文说通Dotnet Core的中间件。
?
首先,我们创建一个接口IWorkService :
public?interface?IWorkService ????{ ????????Task?(); ????} }
再根据IWorkService ,建立一个实体类:
WorkService?:?IWorkService ????{ ????????private?Timer?_timer; ????????0;
????????WorkService(ILogger<WorkService>?logger) ????????{ ????????????_logger?=?logger; ????????}
????????public?async?Task?() ????????{ ????????????var?count?=?Interlocked.Increment(ref?executionCount);
????????????_logger.LogInformation($"Service?proccessing?{count}"); ????????} ????} }
这样就建好了依赖的全部内容。
?
下面,创建托管类:
HostedService?:?IHostedService,?IDisposable ????{ ????????private?readonly?ILogger<HostedService>?_logger; ????????public?IServiceProvider?Services?{?get;?} ????????HostedService(IServiceProvider?services,?ILogger<HostedService>?logger) ????????{ ????????????Services?=?services; ????????????_logger?=?logger; ????????}
??????????() ????????{ ????????????_timer?.Dispose(); ????????}
????????(object?state) ????????{ ????????????_logger.LogInformation("Service?working");
????????????using?(var?scope?=?Services.CreateScope()) ????????????{ ????????????????var?scopedProcessingService?= ????????????????????scope.ServiceProvider ????????????????????????.GetrequiredService<IWorkService>();
????????????????scopedProcessingService.DoWork().GetAwaiter().GetResult(); ????????????} ????????}
????????return?Task.CompletedTask; ????????}
????????return?Task.CompletedTask; ????????} ????} }
把托管类注入到(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 { ????Program ????{ ????????static?AutoResetEvent?_exitEvent;
????????static?async?Task?Main(string[]?args) ????????{ ???????????????? ????????????bool?isRuned; ????????????Mutex?mutex?=?new?Mutex(true,?"OnlyRunOneInstance",?out?isRuned); ????????????if?(!isRuned) ????????????????return;
????????????await?();
???????????????????????? ????????????_exitEvent?=?new?AutoResetEvent(false); ????????????_exitEvent.WaitOne(); ????????}
????????() ????????{ ????????????new?NotImplementedException(); ????????} ????} }
这个模板有两个关键的内容:
- 单实例运行:通常后台任务,只需要有一个实例运行。所以,第一个小段,是解决单实例运行的。多次启动时,除了第一个实例外,其它的实例会自动退出;
- 后台等待:看过很多人写的,在这儿做后台等待时,用了一个无限的循环。类似于下面的:
while(true) { ????Thread.Sleep(1000); }
这种方式也没什么太大的问题。不过,这段代码总是要消耗cpu的计算量,虽然很少,但做为后台任务,或者说Service,毕竟是一种消耗,而且看着不够高大上。
?
当然如果我们需要中断,我们也可以把这个模板改成这样:
string[]?args) ????????{ ????????????return;
????????????_exitEvent?=?false); ????????????(_exitEvent); ????????????_exitEvent.WaitOne(); ????????}
????????(AutoResetEvent?_exitEvent) ????????{ ????????????
????????????_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]
Description=Command
[Service] RestartSec=2s Type=simple
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 加上运行权限:
第三步:注册为Service:
#?systemctl?enable?command.service
完成。
为了配合应用,还需要记住两个命令:启动和关闭Service
#?#启动Service
七、写在后边的话
今天这个文章,是因为前两天,一个兄弟跑过来问我关于数据总线的实现方式,而想到的一个点。
?
很多时候,大家在写代码的时候,会有一种固有的思想:写WebAPI,就想在这个框架中把所有的内容都实现了。这其实不算是一个很好的想法。WebAPI,在业务层面,就应该只是实现简单的处理请求,返回结果的工作,而后台任务跟这个内容截然不同,通常它只做处理,不做返回 --- 事实上也不太好返回,要么客户端等待时间太长,要么客户端已经断掉了。换句话说,用WebAPI实现总线,绝不是一个好的方式。
不过,Console运行为Service,倒是一个总线应用的绝好方式。如果需要按序执行,可以配合MQ服务器,例如RabbitMQ,来实现消息的按序处理。
?
再说代码。很多需求,本来可以用很简单的方式实现。模式这个东西,用来面试,用来讲课,都是很好的内容,但实际开发中,如果有更简单更有效的方式,用起来!Coding的工作是实现,而不是秀技术。当然,能否找到简单有效的方式,这个可能跟实际的技术面有关系。但这并不是一个不能跨越的坎。
多看,多想,每天成长一点点!
?
今天的代码,在:https://github.com/humornif/Demo-Code/tree/master/0012/demo
(全文完)
?
?
|