这是一文说通系列的第二篇,里面有些内容会用到第一篇中间件的部分概念。如果需要,可以参看第一篇:一文说通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]

首先,在这个环境下建立工程:

  1. 创建Solution
% dotnet new sln -o demo
The template "Solution File" was created successfully.
  1. 这次,我们用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.
  1. 把工程加到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需要实现两个方法:StartAsyncStopAsync。其中:

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.AddSingletonservices.AddScopedservices.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();
        }
    }
}

这个模板有两个关键的内容:

  1. 单实例运行:通常后台任务,只需要有一个实例运行。所以,第一个小段,是解决单实例运行的。多次启动时,除了第一个实例外,其它的实例会自动退出;
  2. 后台等待:看过很多人写的,在这儿做后台等待时,用了一个无限的循环。类似于下面的:
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服务器上,我们可能会有一些其它的要求:

  1. 定时运行

Linux上有一个Service,叫cron,是一个用来定时执行程序的服务。

这个服务的设定,需要另一个命令:crontab,位置在/usr/bin下。

具体命令格式这儿不做解释,网上随便查。

  1. 运行到后台

命令后边加个&字符即可:

$ ./command &
  1. 运行为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,来实现消息的按序处理。

再说代码。很多需求,本来可以用很简单的方式实现。模式这个东西,用来面试,用来讲课,都是很好的内容,但实际开发中,如果有更简单更有效的方式,用起来!Coding的工作是实现,而不是秀技术。当然,能否找到简单有效的方式,这个可能跟实际的技术面有关系。但这并不是一个不能跨越的坎。

多看,多想,每天成长一点点!

今天的代码,在:https://github.com/humornif/Demo-Code/tree/master/0012/demo

(全文完)


微信公众号:老王Plus

扫描二维码,关注个人公众号,可以第一时间得到最新的个人文章和内容推送

本文版权归作者所有,转载请保留此声明和原文链接

一文说通Dotnet Core的后台任务的更多相关文章

  1. 一文说通Dotnet Core的中间件

    前几天,公众号后台有朋友在问Core的中间件,所以专门抽时间整理了这样一篇文章.   一.前言 中间件(Middleware)最初是一个机械上的概念,说的是两个不同的运动结构中间的连接件.后来这个概念 ...

  2. 一文说通Dotnet的委托

    简单的概念,也需要经常看看.   一.前言 先简单说说Delegate的由来.最早在C/C++中,有一个概念叫函数指针.其实就是一个内存指针,指向一个函数.调用函数时,只要调用函数指针就可以了,至于函 ...

  3. dotnet core 通过修改文件头的方式隐藏控制台窗口

    原文:dotnet core 通过修改文件头的方式隐藏控制台窗口 在带界面的 dotnet core 程序运行的时候就会出现一个控制台窗口,本文告诉大家使用最简单方法去隐藏控制台窗口. 最近在使用 A ...

  4. Docker 简单发布dotnet core项目 文本版

    原文:https://www.cnblogs.com/chuankang/p/9474591.html docker发布dotnet core简单流程 照着步骤来基本没错 但是有几个要注意的地方: v ...

  5. 北京时间28号0点以后Scott Hanselman同志台宣布dotnet core 1.0 rtm

    今日占住微信号头条的好消息<终于来了!微软.Net Core 1.0下载放出>.本人立马跑到官网http://dot.net看了一下,仍然是.net core 1.0 Preview 1版 ...

  6. Core开发-后台任务利器Hangfire使用

    Core开发-后台任务利器Hangfire使用 ASP.NET Core开发系列之后台任务利器Hangfire 使用. Hangfire 是一款强大的.NET开源后台任务利器,无需Windows服务/ ...

  7. 基于DotNet Core的RPC框架(一) DotBPE.RPC快速开始

    0x00 简介 DotBPE.RPC是一款基于dotnet core编写的RPC框架,而它的爸爸DotBPE,目标是实现一个开箱即用的微服务框架,但是它还差点意思,还仅仅在构思和尝试的阶段.但不管怎么 ...

  8. 手把手教你使用spring cloud+dotnet core搭建微服务架构:服务治理(-)

    背景 公司去年开始使用dotnet core开发项目.公司的总体架构采用的是微服务,那时候由于对微服务的理解并不是太深,加上各种组件的不成熟,只是把项目的各个功能通过业务层面拆分,然后通过nginx代 ...

  9. spring cloud+dotnet core搭建微服务架构:服务发现(二)

    前言 上篇文章实际上只讲了服务治理中的服务注册,服务与服务之间如何调用呢?传统的方式,服务A调用服务B,那么服务A访问的是服务B的负载均衡地址,通过负载均衡来指向到服务B的真实地址,上篇文章已经说了这 ...

随机推荐

  1. PG primary 和 slave 互换

    http://blog.sina.com.cn/s/blog_544a710b0101a122.html http://blog.51cto.com/heyiyi/1898506 https://bl ...

  2. Java Number & Math类

    java Number类 Java的内置数据类型有byte.int.long.double等. Java 语言为每一个内置数据类型提供了对应的包装类. int对应的包装类为Integer long对应 ...

  3. PAT-1057 Stack (树状数组 + 二分查找)

    1057. Stack Stack is one of the most fundamental data structures, which is based on the principle of ...

  4. 0511Object类和异常

    Object类和异常 [要点] toString方法:将类中要打印的信息转换为自定义格式的打印内容 [返回的是当前对象对应的完整包名.类名@当前对象在内存空间首地址(十六进制)] equals方法 p ...

  5. 如何同时关联多个远程仓库,实现一次 push 多站提交(github + gitee)

    这两天做了简陋轮子,主要想放到npm上, Github: canvas-components Gitee: canvas-components github 上一份,gitee 上一份.(走过路过,s ...

  6. 离散数学 II(最全面的知识点汇总)

    离散数学 II(知识点汇总) 目录 离散数学 II(知识点汇总) 代数系统 代数系统定义 例子 二元运算定义 运算及其性质 二元运算的性质 封闭性 可交换性 可结合性 可分配性 吸收律 等幂性 消去律 ...

  7. 【asp.net core 系列】2 控制器与路由的恩怨情仇

    0. 前言 在上一篇文章中,我们初步介绍了asp.net core,以及如何创建一个mvc项目.从这一篇开始,我将为大家展示asp.net core 的各种内容,并且尝试带领大家来挖掘其中的内在逻辑. ...

  8. ES6-for...of与for...in

    1.includes 数组是否包含某个东西 <!DOCTYPE html> <html lang="en"> <head> <meta c ...

  9. Rocket - devices - bootrom

    https://mp.weixin.qq.com/s/PylfNmJDRasTUj9fGp7gLQ 简单介绍bootrom目录中各个文件的实现. 1. Makefile 1) make过程 Makef ...

  10. Rocket - tilelink - WidthWidget

    https://mp.weixin.qq.com/s/pmJcsRMviJZjMwlwYw6OgA   简单介绍WidthWidget的实现.   ​​   1. 基本介绍   用于设定与上游节点连接 ...