相信大家都知道如何在 .NET 中執(zhí)行后臺(tái)(定時(shí))任務(wù)。首先我們會(huì)選擇實(shí)現(xiàn) IHostedService 接口或者繼承BackgroundService 來(lái)實(shí)現(xiàn)后臺(tái)任務(wù)。然后注冊(cè)到容器內(nèi),然后注冊(cè)到容器內(nèi),之后這些后臺(tái)任務(wù) service 就會(huì)自動(dòng)被 觸發(fā)(trigger)。本文不是初級(jí)的入門教程,而是試圖告訴讀者一些容易被忽略的細(xì)節(jié)。
IHostedService
IHostedService 是一個(gè).NET Core 的接口,用于實(shí)現(xiàn)后臺(tái)服務(wù)。通過(guò)實(shí)現(xiàn)這個(gè)接口,你可以在應(yīng)用程序運(yùn)行期間在后臺(tái)執(zhí)行任務(wù),例如定時(shí)任務(wù)、監(jiān)聽事件、處理隊(duì)列等。IHostedService 提供了 StartAsync() 和 StopAsync() 方法,分別用于啟動(dòng)和停止后臺(tái)服務(wù),并且框架會(huì)根據(jù)應(yīng)用程序的生命周期自動(dòng)調(diào)用這兩個(gè)方法。
以下是這個(gè)接口的源碼:
其中 StartAsync
方法由 IApplicationLifetime.ApplicationStarted
事件觸發(fā)
其中 StopAsync
方法由 IApplicationLifetime.ApplicationStopped
事件觸發(fā)
public interface IHostedService
{
Task StartAsync(CancellationToken cancellationToken);
Task StopAsync(CancellationToken cancellationToken);
}
通常我們的后臺(tái)任務(wù)會(huì)被框在一個(gè)while循環(huán)里,定時(shí)去執(zhí)行某些邏輯。以下是我們模擬的一段演示代碼。StartAsync 方法被 call 的時(shí)候就會(huì)執(zhí)行這個(gè) while。代碼很簡(jiǎn)單,不過(guò)多解釋。
public class HostServiceTest_A : IHostedService
{
public async Task StartAsync(CancellationToken cancellationToken)
{
Console.WriteLine("HostServiceTest_A starting.");
while (!cancellationToken.IsCancellationRequested)
{
Console.WriteLine("HostServiceTest_A is doing work.");
await Task.Delay(3000, cancellationToken);
}
}
public Task StopAsync(CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
}
把這個(gè)服務(wù)注冊(cè)到容器內(nèi)。
builder.Services.AddHostedService<HostServiceTest_A>();
下面讓我們啟動(dòng)一下程序試試。可以看到程序可以啟動(dòng),這個(gè) while 循環(huán)也是一直在工作。咋看好像沒(méi)啥問(wèn)題,但是仔細(xì)看看的話好像缺了點(diǎn)什么。
問(wèn)題
對(duì)了,我們這個(gè) ASP.NET Core 程序啟動(dòng)日志沒(méi)有了。也就是整個(gè)程序的啟動(dòng)過(guò)程被 block 住了。原因在于 HostedService 是順序的,一旦某個(gè) HostedService 的 StartAsync 方法沒(méi)有盡快 return 的話,后面所有的任務(wù)全部不能執(zhí)行了。比如你注冊(cè)了多個(gè) HostedService,第一個(gè)使用了這種錯(cuò)誤的方法來(lái)執(zhí)行任務(wù),后面的 HostedService 全部都沒(méi)有機(jī)會(huì)被執(zhí)行。
HostServiceTest_A starting.
HostServiceTest_A is doing work.
HostServiceTest_A is doing work.
HostServiceTest_A is doing work.
HostServiceTest_A is doing work.
···
下面讓我們改進(jìn)一下,使用 Task.Run 來(lái)讓這個(gè)任務(wù)變成異步,并且不去 await 這個(gè) task。
public class HostServiceTest_A : IHostedService
{
public Task StartAsync(CancellationToken cancellationToken)
{
Console.WriteLine("HostServiceTest_A starting.");
Task.Run(async () => {
while (!cancellationToken.IsCancellationRequested)
{
Console.WriteLine("HostServiceTest_A is doing work.");
await Task.Delay(3000, cancellationToken);
}
});
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
}
再次執(zhí)行一下程序,可以看到 HostedService 跟 ASP.NET Core 主程序都可以正確執(zhí)行了。
HostServiceTest_A starting.
HostServiceTest_A is doing work.
info: Microsoft.Hosting.Lifetime[14]
Now listening on: http://localhost:5221
info: Microsoft.Hosting.Lifetime[0]
Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
Hosting environment: Development
info: Microsoft.Hosting.Lifetime[0]
Content root path: D:\workspace\BackgroundServiceDemo\BackgroundServiceDemo
HostServiceTest_A is doing work.
改進(jìn)
我們的后臺(tái)任務(wù)通常是一個(gè)長(zhǎng)期任務(wù),這種情況下更加推薦 LongRunning Task 來(lái) handle 這種任務(wù)。至于為什么可以參考以下文檔:
https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.taskcreationoptions?view=net-9.0
Task.Factory.StartNew(async () => {
while (!cancellationToken.IsCancellationRequested)
{
Console.WriteLine("HostServiceTest_A is doing work.");
await Task.Delay(3000, cancellationToken);
}
}, TaskCreationOptions.LongRunning);
return Task.CompletedTask;
退出
以上我們都在說(shuō)如何啟動(dòng)后臺(tái)任務(wù),還沒(méi)討論如何取消這個(gè)后臺(tái)任務(wù)。參入的那個(gè) cancellationToken 在 Application 被 stop 的時(shí)候并不會(huì)主動(dòng) cancel。所以我們需要在 StopAsync 方法觸發(fā)的時(shí)候手動(dòng)來(lái) Cancel 這個(gè) token。
public class HostServiceTest_A : IHostedService
{
private CancellationTokenSource _cancellationTokenSource;
public Task StartAsync(CancellationToken cancellationToken)
{
Console.WriteLine("HostServiceTest_A starting.");
_cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
Task.Factory.StartNew(async () => {
while (!_cancellationTokenSource.Token.IsCancellationRequested)
{
Console.WriteLine("HostServiceTest_A is doing work.");
await Task.Delay(1000, cancellationToken);
}
Console.WriteLine("HostServiceTest_A task done.");
}, TaskCreationOptions.LongRunning);
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken cancellationToken)
{
if (!cancellationToken.IsCancellationRequested)
{
_cancellationTokenSource.Cancel();
}
Console.WriteLine("HostServiceTest_A stop.");
return Task.CompletedTask;
}
}
讓我們運(yùn)行一下,然后按下 Ctrl + C 來(lái)主動(dòng)退出程序,可以看到我們的 while 被安全退出了。
HostServiceTest_A starting.
HostServiceTest_A is doing work.
info: Microsoft.Hosting.Lifetime[14]
Now listening on: http://localhost:5221
info: Microsoft.Hosting.Lifetime[0]
Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
Hosting environment: Development
info: Microsoft.Hosting.Lifetime[0]
Content root path: D:\workspace\BackgroundServiceDemo\BackgroundServiceDemo
HostServiceTest_A is doing work.
HostServiceTest_A is doing work.
HostServiceTest_A is doing work.
info: Microsoft.Hosting.Lifetime[0]
Application is shutting down...
HostServiceTest_A stop.
HostServiceTest_A task done.
BackgroundService
除了,HostedService
,微軟還給我們提供了 BackgroundService
這個(gè)類。一看這個(gè)類名就知道他能干嘛。其實(shí)也未必想的這么簡(jiǎn)單。BackgroundService 實(shí)際上是 IHostedService 的一個(gè)實(shí)現(xiàn)類。它的核心是將后臺(tái)任務(wù)邏輯放在 ExecuteAsync
這個(gè)抽象方法中。下面我們通過(guò)一個(gè)具體案例來(lái)分析。。
public class BackgroundServiceTest_A : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
Console.WriteLine("ExecuteAsyncA is running.");
await Task.Delay(3000);
}
}
}
運(yùn)行這個(gè)代碼,可以看到 BackgroundService 正常啟動(dòng)了,而且也沒(méi) block 住 ASP.NET Core 的程序??词且磺型昝馈?/p>
ExecuteAsyncA is running.
info: Microsoft.Hosting.Lifetime[14]
Now listening on: http:
info: Microsoft.Hosting.Lifetime[0]
Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
Hosting environment: Development
info: Microsoft.Hosting.Lifetime[0]
Content root path: D:\workspace\BackgroundServiceDemo\BackgroundServiceDemo
ExecuteAsyncA is running.
ExecuteAsyncA is running.
ExecuteAsyncA is running.
ExecuteAsyncA is running.
ExecuteAsyncA is running.
問(wèn)題
以上代碼真的沒(méi)有問(wèn)題嗎?其實(shí)不盡然。讓我們上點(diǎn)強(qiáng)度。如果我們?cè)谘h(huán)中加一個(gè)耗時(shí)很長(zhǎng)的步驟。事實(shí)上這個(gè)很常見(jiàn)。比如以下代碼:
public class BackgroundServiceTest_A : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
Console.WriteLine("ExecuteAsyncA is running.");
LongTermTask();
await Task.Delay(3000);
}
}
private void LongTermTask()
{
Console.WriteLine("LongTermTaskA is doing work.");
Thread.Sleep(30000);
}
}
再次運(yùn)行以下,我們可以發(fā)現(xiàn) ASP.NET Core 的主程序起不來(lái)了,被 block 住了。只有等第一個(gè)循環(huán)周期過(guò)后,主程序才能啟動(dòng)起來(lái)。
ExecuteAsyncA is running.
LongTermTaskA is doing work.
那么問(wèn)題到底出在哪?讓我們看看 BackgroundService
的源碼。
public virtual Task StartAsync(CancellationToken cancellationToken)
{
_stoppingCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
_executeTask = ExecuteAsync(_stoppingCts.Token);
if (_executeTask.IsCompleted)
{
return _executeTask;
}
return Task.CompletedTask;
}
可以看到 StartAsync
方法會(huì)調(diào)用 ExecuteAsync
,但是它沒(méi)有 await 這個(gè)方法,也就是說(shuō) StartAsync
內(nèi)部實(shí)現(xiàn)是個(gè)同步方法。也就是說(shuō) ExecuteAsync
方法跟 StartAsync
會(huì)在同一個(gè)線程上被執(zhí)行(在遇到第一個(gè) await 之前)。如果你注冊(cè)了多個(gè) BackgroundService
并且他們一次 loop 都非常耗時(shí),那么這個(gè)程序啟動(dòng)將會(huì)非常耗時(shí)。其實(shí)微軟已經(jīng)在文檔上提醒大家了:
Avoid performing long, blocking initialization work in ExecuteAsync.
改進(jìn)
那么改進(jìn)方法,同樣使用 Task.Factory.StartNew 來(lái)構(gòu)造一個(gè) LongRunning 的 task 就可以解決。
public class BackgroundServiceTest_A : BackgroundService
{
protected override Task ExecuteAsync(CancellationToken stoppingToken)
{
return Task.Factory.StartNew(async () =>
{
while (!stoppingToken.IsCancellationRequested)
{
Console.WriteLine("HostServiceTest_A is doing work.");
LongTermTask();
await Task.Delay(1000, stoppingToken);
}
Console.WriteLine("HostServiceTest_A task done.");
}, TaskCreationOptions.LongRunning);
}
private void LongTermTask()
{
Console.WriteLine("LongTermTaskA is doing work.");
Thread.Sleep(30000);
}
}
運(yùn)行一下,完美啟動(dòng)后臺(tái)任務(wù)跟主程序。
HostServiceTest_A is doing work.
LongTermTaskA is doing work.
info: Microsoft.Hosting.Lifetime[14]
Now listening on: http://localhost:5221
info: Microsoft.Hosting.Lifetime[0]
Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
Hosting environment: Development
info: Microsoft.Hosting.Lifetime[0]
Content root path: D:\workspace\BackgroundServiceDemo\BackgroundServiceDemo
繼續(xù)改進(jìn)
如果要繼續(xù)吹毛求疵的話,我們還可以改進(jìn)一下。從 .NET6 開始 PeriodicTimer
被加入進(jìn)來(lái)。它是一個(gè) timer,可以替換一部分 Task.Delay
活。使用 PeriodicTimer
話相對(duì)于 Task.Delay
來(lái)說(shuō)可以讓 loop 的間隔更加精準(zhǔn)的被控制。
詳見(jiàn)這里 https://learn.microsoft.com/en-us/dotnet/api/system.threading.periodictimer.waitfornexttickasync?view=net-9.0
protected override Task ExecuteAsync(CancellationToken stoppingToken)
{
return Task.Factory.StartNew(async () =>
{
var timer = new PeriodicTimer(TimeSpan.FromSeconds(1));
while (await timer.WaitForNextTickAsync(stoppingToken))
{
Console.WriteLine("HostServiceTest_A is doing work.");
LongTermTask();
}
Console.WriteLine("HostServiceTest_A task done.");
}, TaskCreationOptions.LongRunning);
}
總結(jié)
通過(guò)以上的演示,我們可以感受到,實(shí)現(xiàn)一個(gè)后臺(tái)任務(wù)還是有非常多的點(diǎn)需要被注意的。特別是不要在 StartAsync 或者 ExcuteAsync
方法內(nèi)執(zhí)行耗時(shí)的同步方法。如果有耗時(shí)任務(wù)請(qǐng)包裹在新的 Task 內(nèi)執(zhí)行。我們要保證這兩個(gè)方法輕量化能夠被快速的執(zhí)行完畢,這樣的話不會(huì)影響應(yīng)用程序的啟動(dòng)。
轉(zhuǎn)自https://www.cnblogs.com/kklldog/p/19020718