諾禾、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]

首先,在這個環境下樹立工程:

創立Solution
% dotnet new sln -o demo
The template “Solution File” was created successfully.
這次,我們用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.
把工程加到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需求完成兩個辦法:StartAsync和StopAsync。其中:

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 _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.AddSingleton、services.AddScoped和services.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();
    }
}

}
這個模板有兩個關鍵的內容:

單實例運轉:通常後臺任務,只需求有一個實例運轉。所以,第一個小段,是處理單實例運轉的。屢次啓動時,除了第一個實例外,其它的實例會自動退出;
後臺等候:看過很多人寫的,在這兒做後臺等候時,用了一個無限的循環。相似於下面的:
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效勞器上,我們可能會有一些其它的請求:

定時運轉
Linux上有一個Service,叫cron,是一個用來定時執行程序的效勞。

這個效勞的設定,需求另一個命令:crontab,位置在/usr/bin下。

詳細命令格式這兒不做解釋,網上隨意查。

運轉到後臺
命令後邊加個&字符即可:

$ ./command &
運轉爲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,來完成音訊的按序處置。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章