百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术文章 > 正文

如何正确实现一个后台(定时)任务(后台定时任务怎么实现)

ccwgpt 2025-07-27 19:14 1 浏览 0 评论


相信大家都知道如何在 .NET 中执行后台(定时)任务。首先我们会选择实现 IHostedService 接口或者继承BackgroundService 来实现后台任务。然后注册到容器内,然后注册到容器内,之后这些后台任务 service 就会自动被 触发(trigger)。本文不是初级的入门教程,而是试图告诉读者一些容易被忽略的细节。

IHostedService

IHostedService 是一个.NET Core 的接口,用于实现后台服务。通过实现这个接口,你可以在应用程序运行期间在后台执行任务,例如定时任务、监听事件、处理队列等。IHostedService 提供了 StartAsync() 和 StopAsync() 方法,分别用于启动和停止后台服务,并且框架会根据应用程序的生命周期自动调用这两个方法。
以下是这个接口的源码:

其中 StartAsync 方法由 IApplicationLifetime.ApplicationStarted 事件触发 其中 StopAsync 方法由 IApplicationLifetime.ApplicationStopped 事件触发

 //
// 摘要:
// Defines methods for objects that are managed by the host.
public interface IHostedService
{

Task StartAsync(CancellationToken cancellationToken);

Task StopAsync(CancellationToken cancellationToken);
}

通常我们的后台任务会被框在一个while循环里,定时去执行某些逻辑。以下是我们模拟的一段演示代码。StartAsync 方法被 call 的时候就会执行这个 while。代码很简单,不过多解释。

 public class HostServiceTest_A : IHostedService
{
public async Task StartAsync(CancellationToken cancellationToken)
{
Console.WriteLine("HostServiceTest_A starting.");

while (!cancellationToken.IsCancellationRequested)
{
// Simulate some work
Console.WriteLine("HostServiceTest_A is doing work.");

await Task.Delay(3000, cancellationToken); // Delay for 3 second
}
}

public Task StopAsync(CancellationToken cancellationToken)
{
// to do

return Task.CompletedTask;
}
}

把这个服务注册到容器内。

 builder.Services.AddHostedService<HostServiceTest_A>();

下面让我们启动一下程序试试。可以看到程序可以启动,这个 while 循环也是一直在工作。乍看好像没啥问题,但是仔细看看的话好像缺了点什么。

问题

对了,我们这个 ASP.NET Core 程序启动日志没有了。也就是整个程序的启动过程被 block 住了。原因在于 HostedService 是顺序的,一旦某个 HostedService 的 StartAsync 方法没有尽快 return 的话,后面所有的任务全部不能执行了。比如你注册了多个 HostedService,第一个使用了这种错误的方法来执行任务,后面的 HostedService 全部都没有机会被执行。

HostServiceTest_A starting.
HostServiceTest_A is doing work.
HostServiceTest_A is doing work.
HostServiceTest_A is doing work.
HostServiceTest_A is doing work.
···

下面让我们改进一下,使用 Task.Run 来让这个任务变成异步,并且不去 await 这个 task。

 public class HostServiceTest_A : IHostedService
{
public Task StartAsync(CancellationToken cancellationToken)
{
Console.WriteLine("HostServiceTest_A starting.");

Task.Run(async () => {
while (!cancellationToken.IsCancellationRequested)
{
// Simulate some work
Console.WriteLine("HostServiceTest_A is doing work.");

await Task.Delay(3000, cancellationToken); // Delay for 3 second
}
});

return Task.CompletedTask;
}

public Task StopAsync(CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
}

再次执行一下程序,可以看到 HostedService 跟 ASP.NET Core 主程序都可以正确执行了。

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.

改进

我们的后台任务通常是一个长期任务,这种情况下更加推荐 LongRunning Task 来 handle 这种任务。至于为什么可以参考以下文档: https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.taskcreationoptions?view=net-9.0

 Task.Factory.StartNew(async () => {
while (!cancellationToken.IsCancellationRequested)
{
// Simulate some work
Console.WriteLine("HostServiceTest_A is doing work.");

await Task.Delay(3000, cancellationToken); // Delay for 3 second
}
}, TaskCreationOptions.LongRunning);

return Task.CompletedTask;

退出

以上我们都在说如何启动后台任务,还没讨论如何取消这个后台任务。参入的那个 cancellationToken 在 Application 被 stop 的时候并不会主动 cancel。所以我们需要在 StopAsync 方法触发的时候手动来 Cancel 这个 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)
{
// Simulate some work
Console.WriteLine("HostServiceTest_A is doing work.");

await Task.Delay(1000, cancellationToken); // Delay for 3 second
}

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;
}
}

让我们运行一下,然后按下 Ctrl + C 来主动退出程序,可以看到我们的 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 这个类。一看这个类名就知道他能干嘛。其实也未必想的这么简单。BackgroundService 实际上是 IHostedService 的一个实现类。它的核心是将后台任务逻辑放在 ExecuteAsync 这个抽象方法中。下面我们通过一个具体案例来分析。。

 public class BackgroundServiceTest_A : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
Console.WriteLine("ExecuteAsyncA is running.");

await Task.Delay(3000);
}
}
}

运行这个代码,可以看到 BackgroundService 正常启动了,而且也没 block 住 ASP.NET Core 的程序。看是一切完美。

ExecuteAsyncA is running.
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
ExecuteAsyncA is running.
ExecuteAsyncA is running.
ExecuteAsyncA is running.
ExecuteAsyncA is running.
ExecuteAsyncA is running.

问题

以上代码真的没有问题吗?其实不尽然。让我们上点强度。如果我们在循环中加一个耗时很长的步骤。事实上这个很常见。比如以下代码:

 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()
{
// Simulate some work
Console.WriteLine("LongTermTaskA is doing work.");
Thread.Sleep(30000);
}
}

再次运行以下,我们可以发现 ASP.NET Core 的主程序起不来了,被 block 住了。只有等第一个循环周期过后,主程序才能启动起来。

ExecuteAsyncA is running.
LongTermTaskA is doing work.

那么问题到底出在哪?让我们看看 BackgroundService 的源码。

 public virtual Task StartAsync(CancellationToken cancellationToken)
{
// Create linked token to allow cancelling executing task from provided token
_stoppingCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);

// Store the task we're executing
_executeTask = ExecuteAsync(_stoppingCts.Token);

// If the task is completed then return it, this will bubble cancellation and failure to the caller
if (_executeTask.IsCompleted)
{
return _executeTask;
}

// Otherwise it'
s running
return Task.CompletedTask;
}

可以看到 StartAsync 方法会调用 ExecuteAsync,但是它没有 await 这个方法,也就是说 StartAsync 内部实现是个同步方法。也就是说 ExecuteAsync 方法跟 StartAsync 会在同一个线程上被执行(在遇到第一个 await 之前)。如果你注册了多个 BackgroundService 并且他们一次 loop 都非常耗时,那么这个程序启动将会非常耗时。其实微软已经在文档上提醒大家了:

Avoid performing long, blocking initialization work in ExecuteAsync.

改进

那么改进方法,同样使用 Task.Factory.StartNew 来构造一个 LongRunning 的 task 就可以解决。

 public class BackgroundServiceTest_A : BackgroundService
{
protected override Task ExecuteAsync(CancellationToken stoppingToken)
{
return Task.Factory.StartNew(async () =>
{
while (!stoppingToken.IsCancellationRequested)
{
// Simulate some work
Console.WriteLine("HostServiceTest_A is doing work.");

LongTermTask();

await Task.Delay(1000, stoppingToken); // Delay for 1 second
}

Console.WriteLine("HostServiceTest_A task done.");

}, TaskCreationOptions.LongRunning);
}

private void LongTermTask()
{
// Simulate some work
Console.WriteLine("LongTermTaskA is doing work.");
Thread.Sleep(30000);
}
}

运行一下,完美启动后台任务跟主程序。

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

继续改进

如果要继续吹毛求疵的话,我们还可以改进一下。从 .NET6 开始 PeriodicTimer 被加入进来。它是一个 timer,可以替换一部分 Task.Delay 活。使用 PeriodicTimer 话相对于 Task.Delay 来说可以让 loop 的间隔更加精准的被控制。

 protected override Task ExecuteAsync(CancellationToken stoppingToken)
{
return Task.Factory.StartNew(async () =>
{
var timer = new PeriodicTimer(TimeSpan.FromSeconds(1));

while (await timer.WaitForNextTickAsync(stoppingToken))
{
// Simulate some work
Console.WriteLine("HostServiceTest_A is doing work.");
LongTermTask();
}

Console.WriteLine("HostServiceTest_A task done.");

}, TaskCreationOptions.LongRunning);
}

总结

通过以上的演示,我们可以感受到,实现一个后台任务还是有非常多的点需要被注意的。特别是不要在 StartAsync 或者 ExcuteAsync 方法内执行耗时的同步方法。如果有耗时任务请包裹在新的 Task 内执行。我们要保证这两个方法轻量化能够被快速的执行完毕,这样的话不会影响应用程序的启动。

参考:
https://learn.microsoft.com/en-us/dotnet/api/system.threading.periodictimer.waitfornexttickasync?view=net-9.0

https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.taskcreationoptions?view=net-9.0

https://learn.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services?view=aspnetcore-9.0&tabs=visual-studio


相关推荐

Spring框架基础知识-第四节内容(Spring基础配置)

Spring基础配置Spring框架本身有四大原则:(1)使用POJO进行轻量级和最小侵入式开发。(2)通过依赖注入和基于接口编程实现松耦合。(3)通过AOP和默认习惯进行声明式编程。(4)使...

SpringBoot项目开发实战销售管理系统——项目框架搭建!

项目框架搭建在完成项目的分析和数据库设计后,一般由架构师完成项目框架的搭建,包括项目依赖的添加、项目的配置和项目日志的配置,完成后再开始业务代码的编写。技术栈的搭建新建一个SpringBoot项目,...

从零到一:独立运行若依框架系统并进行本地二次开发

####一、环境准备1.**基础环境**:-JDK1.8+(推荐JDK17)-Maven3.6+-MySQL5.7+(推荐8.0)-Redis5.0+-Node.js16...

单片机时间片轮询程序架构(单片机如何实现精准的时间周期)

时间片轮询法有很多时候都是与操作系统一起被提到,也就是说很多时候是操作系统中使用了这一方法:STM32单片机开发中的RTOS。下文将参考别人的代码,演示建立的一个时间片轮询架构程序的...

Netty主要组件和服务器启动源码分析

1.Netty服务端启动代码publicclassNettyServer{publicstaticvoidmain(String[]args)throwsInterrup...

前端定时任务的神库!快把它加到你的项目中去!

我们常会遇到定时刷新数据、轮询接口、发送提醒等场景,我们常会遇到定时刷新数据、轮询接口、发送提醒等场景。为什么选择cron库?定时任务开发痛点原生setInterval的时间误差累积难以实现复杂的...

如何正确实现一个后台(定时)任务(后台定时任务怎么实现)

相信大家都知道如何在.NET中执行后台(定时)任务。首先我们会选择实现IHostedService接口或者继承BackgroundService来实现后台任务。然后注册到容器内,然后注册到容...

秒杀传统的Linux Crontab,这款开源的定时任务管理系统绝了!

Gocron是一款开源的定时任务管理系统,基于Go语言开发,旨在替代传统的LinuxCrontab。它通过Web界面提供直观的任务管理功能,支持精确到秒的Crontab时间表达式,并具备任务重试、超...

Python 定时任务:schedule 自动执行脚本太方便。

2025年了,还在为Python定时任务头疼?轻量级需求搞什么Celery,schedule三行代码就搞定。这库把定时任务简化到像说人话,但新手直接抄文档容易踩坑。文档只会告诉你怎么设置每10分钟执行...

SpringBoot扩展——定时任务!(基于springboot的校园宿舍管理系统的设计与实现)

定时任务项目开发中会涉及很多需要定时执行的代码,如每日凌晨对前一日的数据进行汇总,或者系统缓存的清理、对每日的数据进行分析和总结等需求,这些都是定时任务。单体系统和分布式系统的分布式任务有很大的区别,...

适合普通开发者和产品经理的PHP应用模板开发AI的SaaS应用框架

简单到傻!Liang_SaaS适合普通开发者和产品经理的PHP应用模板开发AI的SaaS应用框架,利用Php开发AI的SaaS应用框架,是一个强大的内容管理仪表板模板,基于Bootstrap和...

非常实用的15款开源PHP类库(php开源管理系统)

PHP库给开发者提供了一个标准接口,它帮助开发者在PHP里充分利用面向对象编程。这些库为特定类型的内置功能提供了一个标准的API,允许类可以与PHP引擎进行无缝的交互。此外,开发者使用这些类库还可以简...

蜂神榜苹果商店也凑热闹:“520”我爱玩家!

各位看官,今天被朋友圈各类“520”刷屏呢?有没有给你亲爱的家人一份“520”模式的红包呢?苹果商店也给了玩家一个“520”模式的惊喜---再一次提供了多款“1元”价格的游戏!并且此次降价的游戏品质都...

变成气球的猫咪《气球》十一正式推出

墨西哥游戏公司NoodlecakeGames曾开发过《致命框架》、《阿尔托冒险》等优秀佳作,而它旗下的最新游戏《气球》(TheBalloons)在十一的时候就要和大家见面了。游戏中,玩家要操控娃娃...

星座超游爱:狮子遇挑战,处女手抓牢~

teemo跟大家讲了三期太阳星座,也许有很多不热心的小伙伴并不知道是什么东西,今天就小科普一番~在出生的那一天,太阳所落的那个星座,就是每个人的太阳星座,而这恰好就是大家的性格中心,是权势驱力、人格的...

取消回复欢迎 发表评论: