zoukankan      html  css  js  c++  java
  • 一文说通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 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

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

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

  • 相关阅读:
    HTML页面空格记录&nbsp;&ensp; &emsp; (小计)
    JS对象、构造器函数和原型对象之间的关系
    情人节,送女友一桶代码可否?
    JavaScript中的BOM和DOM
    js中字符替换函数String.replace()使用技巧
    XML DOM 节点类型(Node Types)
    node 基础
    npm脚本命令npm run script的使用
    Node.js学习笔记六,获取get/post请求的参数
    querystring模块详解
  • 原文地址:https://www.cnblogs.com/tiger-wang/p/13081020.html
Copyright © 2011-2022 走看看