诺禾、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();
    }

    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,来完成音讯的按序处置。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章