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

    我这里的功能是弹出 右侧搜索 的页面: top.layui.admin.popupRight({ id: 'LAY_business_PopupLayer' ,area: '350px' ,succe ...

  2. oracle关于rownum的使用【oracle】

    转自:https://blog.csdn.net/qiuzhi__ke/article/details/78892822 关于rownum是怎么产生的(网上有不少的文章,下面是摘录): rownum是 ...

  3. Lowest Common Multiple Plus(hdu2028)

    思考: 乘法爆咋数据.把int换成unsigned就过了,同时%d换成%u.求最大公约数和最小公倍数. #include<stdio.h> int gcd(unsigned x, unsi ...

  4. web项目——javax.servlet.ServletException: Circular view path [registerForm]

    报错: 控制台输出: 三月 21, 2019 10:12:32 上午 org.springframework.web.servlet.PageNotFound noHandlerFound 警告: N ...

  5. PhpStorm2016.3激活

    选择License server,输入以下任意一个地址: http://idea.imsxm.com/http://114.215.133.70:41017/http://mcpmcc.com:101 ...

  6. Js 改变时间格式输出格式

    朋友看到的方法,非js原生的 自己封装到 function date2str(x,y) { var z={y:x.getFullYear(),M:x.getMonth()+1,d:x.getDate( ...

  7. ES[7.6.x]学习笔记(十)聚合查询

    聚合查询,它是在搜索的结果上,提供的一些聚合数据信息的方法.比如:求和.最大值.平均数等.聚合查询的类型有很多种,每一种类型都有它自己的目的和输出.在ES中,也有很多种聚合查询,下面我们看看聚合查询的 ...

  8. Oracle数字格式化

    @ 目录 Oracle数字格式化 开发中的常见问题 数字格式模型元素 Oracle数字格式化 A format model is a character literal that describes ...

  9. Qt如何管理组件

    转载:清凉简装的博客 解决“要继续此操作,至少需要一个有效且已启用的储存库“问题 1.在Qt安装目录找到组件管理软件MaintenanceTool,双击. 2.点击下一步,出现要继续此操作,至少需要一 ...

  10. CF652E Pursuit For Aritifacts

    题目传送门 这是一道很好的练习强联通的题目. 首先,从题中可以看到,题目的要求就是要我们求出从起点到终点是否可以经过flag = 1 的边. 由于是无向图,且要求很多,直接暴力dfs会很凌乱. 那么, ...