一文说通Dotnet Core的后台任务

这是一文说通系列的第二篇,里面有些内容会用到第一篇中间件的部分概念。如果需要,可以参看第一篇:一文说通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 demoThe template "Solution File" was created successfully.
  1. 这次,我们用Webapi创建工程

% cd demo% dotnet new webapi -o webapidemoThe 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 consoledemoThe 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=2sType=simple# 执行应用的默认用户。应用如果没有特殊要求,最好别用root运行User=your_user_nameGroup=your_group_name# 应用的目录,绝对路径WorkingDirectory=your_app_folder# 应用的启动路径ExecStart=your_app_folder/your_appRestart=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

(全文完)


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

(0)

相关推荐

  • 详解Net Core Web Api项目与在NginX下发布

    前言 本文将介绍Net Core的一些基础知识和如何NginX下发布Net Core的WebApi项目. 测试环境 操作系统:windows 10 开发工具:visual studio 2019 框架 ...

  • 面向.NET开发人员的Dapr——入门

    菜鸟吊思 dotNET跨平台 今天 目录: 面向.NET开发人员的Dapr--前言 面向.NET开发人员的Dapr--分布式世界 面向.NET开发人员的Dapr--俯瞰Dapr Get started ...

  • C# 异步编程

    基于Task的异步编程模式(TAP)是Microsoft为.Net平台下使用Task进行编程所提供的一组建议,这种模式提供了可以被await消耗(调用)方法的APIs,并且当使用async关键字编写遵 ...

  • VSC 创建 Net Core 3.0 版本 WebAPI

    首先确保安装好了VSC(这个算是废话......), 并且为VSC安装了Microsoft提供的C#插件, 以及.Net Core 3.0, 然后就可以开始了 1. 使用VSC打开已经创建好的文件夹( ...

  • 打造跨平台.NET Core后台服务

    续之前讲的在TopShelf上部署ASP.NET Core程序,作为后台服务运行,自从.NET Core 3.0出现以后,出现了自带的Generic Host,使得自托管服务变为可能.这种方式和Top ...

  • 动手实现一个适用于.NET Core 的诊断工具

    今天 以下文章来源于全球技术精选 ,作者SpringLeee 全球技术精选dotnet 国内外新鲜技术资讯,优秀博客,开源项目分享平台 前言 大家可能对诊断工具并不陌生,从大名鼎鼎的 dotTrace ...

  • 一文说通Dotnet Core的中间件

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

  • dotNET Core WebAPI 统一处理(返回值、参数验证、异常)

    现在 Web 开发比较流行前后端分离,我们的产品也是一样,前端使用Vue,后端使用 dotNet Core WebAPI ,在写 API 的过程中有很多地方需要统一处理: 文档 参数验证 返回值 异常 ...

  • 温故知新,DotNet Core SDK和.Net CLI十八般武艺

    简介 .NET命令行接口 (CLI) 工具是用于开发.生成.运行和发布.NET应用程序的跨平台工具链. https://docs.microsoft.com/zh-cn/dotnet/core/too ...

  • dotNET Core 3.X 使用 Web API

    现在的 Web 开发大多都是前后端分离的方式,后端接口的正确使用显得尤为重要,本文讲下在 dotNET Core 3.X 下使用 Web API . 环境 操作系统:Mac IDE:Rider dot ...

  • dotNET Core 3.X 使用 Jwt 实现接口认证

    在前后端分离的架构中,前端需要通过 API 接口的方式获取数据,但 API 是无状态的,没有办法知道每次请求的身份,也就没有办法做权限的控制.如果不做控制,API 就对任何人敞开了大门,只要拿到了接口 ...

  • 一文入门.NET Core操作ElasticSearch 7.x

    原创 青城 青城同学 1周前在互联网上,随处可见的搜索框.背后所用的技术大多数就是全文检索.在全文检索领域,常见的库/组件有:Lucene.Solr.Sphinx.ElasticSearch等.简单对 ...

  • Azure 无服务器 Function 函数计算服务 dotnet core 3.1 创建和部署入门

    本文用的是 世纪互联 的 Azure.cn 版本,这个版本因为是在国内,所以网速会快超级超级多.使用 世纪互联 的版本需要一块钱哦,用一块钱就能进入一个月的免费试用.本文主要告诉小伙伴如何使用 Azu ...

  • WPF dotnet core 的 Blend SDK Behaviors 库

    之前版本是通过安装 Blend SDK 支持 Behaviors 库的,但是这个方法都是通过引用 dll 的方式,不够优雅.在升级到 dotnet core 3.0 的时候就需要使用 WPF 官方团队 ...

  • dotNET Core 3.X 依赖注入

    如果说在之前的 dotNET 版本中,依赖注入还是个比较新鲜的东西,那么在 dotNET Core 中已经是随处可见了,可以说整个 dotNET Core 的框架是构建在依赖注入框架之上.本文讲解下对 ...