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

    1.列约束 (1)主键约束——PRIMARY KEY (2)非空约束——NOT NULL 声明了非空约束的列上,不允许使用NULL (3)唯一约束——UNIQUE 声明了唯一约束的列上不能插入重复的值 ...

  2. 1.3Go环境搭建之Windows

    1.1.2. Golang SDK SDK 的全称(Software Development Kit 软件开发工具包) 2) SDK是提供给开发人员使用的,其中包含了对应开发语言的工具包 1.1.3. ...

  3. Python一切皆是对象,但这和内存管理有什么关系?

    本文始发于个人公众号:TechFlow,原创不易,求个关注 今天是Python的第15篇文章,我们来聊聊Python中内存管理机制,以及循环引用的问题. Python的内存管理机制 对于工程师而言,内 ...

  4. Alink漫谈(四) : 模型的来龙去脉

    Alink漫谈(四) : 模型的来龙去脉 目录 Alink漫谈(四) : 模型的来龙去脉 0x00 摘要 0x01 模型 1.1 模型包含内容 1.2 Alink的模型文件 0x02 流程图 0x03 ...

  5. poj2449第K小路径问题

    Remmarguts' Date Time Limit: 4000MS   Memory Limit: 65536K Total Submissions: 30017   Accepted: 8159 ...

  6. C语言经典笔试题目

    1.bool,float,指针变量 与 “零值” 比较的if语句 注意点:c语言中bool类型采用整数存储,0为false,非0均为true; float类型采用IEEE754标准,第一位符号位,中间 ...

  7. 启动独立的tomcat服务器,没有自动创建ServletContext,对Context生命周期的监听失败

    1.可能web.xml文件里对ContextListener没有进行配置 2.web.xml文件有关对ContextListener的配置,出现了错误的单词拼写问题 比如 <listener&g ...

  8. Android设置按钮透明

    <Button android:id="@+id/bt3" android:layout_width="163dp" android:layout_hei ...

  9. #442-Find All Duplicates in an Array-数组中重复的数字

    一.题目 给定一个整数数组 a,其中1 ≤ a[i] ≤ n (n为数组长度), 其中有些元素出现两次而其他元素出现一次. 找到所有出现两次的元素. 你可以不用到任何额外空间并在O(n)时间复杂度内解 ...

  10. linux-offen-used-commands

    文件系统 cd 进入目录 ls 列出目录信息,ls -al (或 ll)列出详细信息 touch 新建文件 mkdir 新建目录 rm 删除文件或目录 cp 复制 mv 移动(或重命名) 搜索.查找. ...