一、前言
後臺任務在一些特殊的應用場所,有相當的需求。
比如,我們需求完成一個定時任務、或週期性的任務、或非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,來完成音訊的按序處置。