如何在 ASP.NET Core 2.1 中的计时器上运行 BackgroundService

Posted

技术标签:

【中文标题】如何在 ASP.NET Core 2.1 中的计时器上运行 BackgroundService【英文标题】:How to run BackgroundService on a timer in ASP.NET Core 2.1 【发布时间】:2019-05-12 15:54:03 【问题描述】:

我想在 ASP.NET Core 2.1 中运行后台作业。它必须每 2 小时运行一次,并且需要访问我的 DI 容器,因为它会在数据库中执行一些清理工作。它需要是async,并且它应该独立于我的 ASP.NET Core 2.1 应用程序运行。

我看到有一个 IHostedService,但是 ASP.NET Core 2.1 还引入了一个名为 BackgroundService 的抽象类,它为我做了更多的工作。看起来不错,我想用那个!

不过,我无法弄清楚如何在计时器上运行源自 BackgroundService 的服务。

我是否需要在 ExecuteAsync(token) 中配置它,方法是记住它上次运行的时间并确定这是否是 2 小时,或者是否有更好/更清洁的方法可以在某个地方说它必须每 2 小时运行一次?

另外,我使用BackgroundService 解决问题的方法是否正确?

谢谢!

编辑:

发布到MS extensions repo

【问题讨论】:

A timed background service 是文档中的示例之一。检查Background tasks with hosted services in ASP.NET Core。 嗯,我明白了。问题是,我看到 DoWork() 不是异步的。我可以标记DoWork async,但这并不是真正正确的方式,因为它不会被等待(?) @PanagiotisKanavos 如果您有答案,请将其写为实际答案,以便当您的答案帮助我解决这个问题时,我可以将其标记为已完成 :) 您能告诉我为什么实现IHostedService 然后使用计时器会比使用BackgroundService 并通过执行计时器检查来检查您是否想在ExecuteAsync 中运行您的工作更好? (再次,发布你的答案+为什么这比这种方法更好的原因作为答案)我知道我的方法会导致ExecuteAsync 被调用,如果它不会被执行,但是我的问题是:什么是如果你不能把它放在计时器上,BackgroundService 的点?追问:那为什么没有TimedBackgroundService呢? 请谨慎使用带计时器的托管服务,因为 IIS 每 20 分钟回收一次,您的托管服务将同时停止。因此,您需要将应用程序池设置为始终开启,这可能会导致泄漏或内存问题。 【参考方案1】:

04-2020 更新,在底部阅读!

@Panagiotis Kanavos 在我的问题的 cmets 中给出了答案,但并未将其作为实际答案发布;这个答案是献给他/她的。

我使用Timed background service 就像 Microsoft 文档中的那个来创建服务。

internal class TimedHostedService : IHostedService, IDisposable

    private readonly ILogger _logger;
    private Timer _timer;

    public TimedHostedService(ILogger<TimedHostedService> logger)
    
        _logger = logger;
    

    public Task StartAsync(CancellationToken cancellationToken)
    
        _logger.LogInformation("Timed Background Service is starting.");

        _timer = new Timer(DoWork, null, TimeSpan.Zero, 
            TimeSpan.FromSeconds(5));

        return Task.CompletedTask;
    

    private void DoWork(object state)
    
        _logger.LogInformation("Timed Background Service is working.");
    

    public Task StopAsync(CancellationToken cancellationToken)
    
        _logger.LogInformation("Timed Background Service is stopping.");

        _timer?.Change(Timeout.Infinite, 0);

        return Task.CompletedTask;
    

    public void Dispose()
    
        _timer?.Dispose();
    

在我的例子中,我通过 new Timer(async () =&gt; await DoWorkAsync(), ...) 异步调用了 _timer

将来,可以编写一个扩展,使这样的类在 Extensions repo 中可用,因为我认为这非常有用。我在描述中发布了github问题链接。

提示,如果您计划将此类重用于多个托管服务,请考虑创建一个包含计时器和抽象 PerformWork() 或其他内容的基类,以便“时间”逻辑仅在一个位置。

感谢您的回答!我希望这对将来的某人有所帮助。

04-2020 年更新

使用开箱即用的普通核心服务集合 DI 容器无法在此处注入作用域服务。由于注册错误,我使用了 autofac,这使得在构造函数中使用像 IClassRepository 这样的范围服务成为可能,但是当我开始处理一个只使用 AddScoped&lt;&gt;(), AddSingleton&lt;&gt;(), AddTransient&lt;&gt;() 的不同项目时,我们发现注入范围的东西不起作用,因为您不在作用域上下文中。

为了使用您的作用域服务,注入一个IServiceScopeFactory(更易于测试)并使用CreateScope(),它允许您使用scope.GetService()using 语句:)

【讨论】:

这不会使计时器异步,new Timer(async () =&gt; await DoWorkAsync() 默认情况下,计时器将按定时计划执行您的函数,而不管其他函数是否仍在执行。此外,如果没有请求,您的计时器将不会执行,请参阅question 我对计时器有同样的误解,答案解释了 对于每个登录到应用程序的用户,后台服务将首先触发,后续触发在上述时间之后 别忘了在 Startup.cs 中使用“services.AddHostedService();”注册服务。【参考方案2】:

实现这一点的一种方法是使用 HangFire.io,它将处理计划的后台任务,管理服务器之间的平衡,并且具有相当大的可扩展性。

在https://www.hangfire.io查看定期工作

【讨论】:

我想补充一点,.NET Core 提供了一个完全免费的解决方案。 HangFire 将根据您的用例花钱。正如我在帖子中提到的,我想使用 ASP.NET Core 的解决方案,因为它存在;我只是不知道如何使用它。为这样的事情使用 3rd 方解决方案似乎有点奇怪,因为我想做的并不复杂。 当然。如果您使用 SQL Server,HangFire 是免费的,它提供了一个完整的解决方案,您可以实施然后继续开发。 (注意:我不隶属于)但确定我理解您的要求,只是想提供帮助。 “如果你使用 SQL 服务器,HangFire 是免费的”。你能提供一个链接吗?此外,我确实重视您的回复,只是它不是我想要使用的 ASP.NET Core 功能的一部分,因此我觉得它不应该成为帖子的答案。 :) hangfire.io/pricing 你在这里 - 第一列“打开” - LGPL 3.0 下的 Hangfire Core,商业用途【参考方案3】:

这是基于之前的回复和https://***.com/a/56666084/1178572的改进版本

改进:

    在前一个任务完成执行之前,它不会启动计时器。这将有助于避免陷入同时执行两个任务的情况。 它支持异步任务 它处理任务执行期间可能出现的异常,以确保它不会阻止下一个任务的执行。 为执行范围的每个任务创建一个范围,因此您可以在 RunJobAsync 中访问任何范围内的服务 您可以在继承的类中指定间隔和初始任务执行时间。

访问范围服务示例

    protected override async Task RunJobAsync(IServiceProvider serviceProvider, CancellationToken stoppingToken)
    
            DbContext context = serviceProvider.GetRequiredService<DbContext>();
    

源代码:

public abstract class TimedHostedService : IHostedService, IDisposable

    private readonly ILogger _logger;
    private Timer _timer;
    private Task _executingTask;
    private readonly CancellationTokenSource _stoppingCts = new CancellationTokenSource();

    IServiceProvider _services;
    public TimedHostedService(IServiceProvider services)
    
        _services = services;
        _logger = _services.GetRequiredService<ILogger<TimedHostedService>>();
        
    

    public Task StartAsync(CancellationToken cancellationToken)
    
        _timer = new Timer(ExecuteTask, null,FirstRunAfter, TimeSpan.FromMilliseconds(-1));

        return Task.CompletedTask;
    

    private void ExecuteTask(object state)
    
        _timer?.Change(Timeout.Infinite, 0);
        _executingTask = ExecuteTaskAsync(_stoppingCts.Token);
    

    private async Task ExecuteTaskAsync(CancellationToken stoppingToken)
    
        try
        
            using (var scope = _services.CreateScope())
            
                await RunJobAsync(scope.ServiceProvider, stoppingToken);
            
        
        catch (Exception exception)
        
            _logger.LogError("BackgroundTask Failed", exception);
        
        _timer.Change(Interval, TimeSpan.FromMilliseconds(-1));
    

    /// <summary>
    /// This method is called when the <see cref="IHostedService"/> starts. The implementation should return a task 
    /// </summary>
    /// <param name="serviceProvider"></param>
    /// <param name="stoppingToken">Triggered when <see cref="IHostedService.StopAsync(CancellationToken)"/> is called.</param>
    /// <returns>A <see cref="Task"/> that represents the long running operations.</returns>
    protected abstract Task RunJobAsync(IServiceProvider serviceProvider, CancellationToken stoppingToken);
    protected abstract TimeSpan Interval  get; 
    
    protected abstract TimeSpan FirstRunAfter  get; 
    
    public virtual async Task StopAsync(CancellationToken cancellationToken)
    
        _timer?.Change(Timeout.Infinite, 0);

        // Stop called without start
        if (_executingTask == null)
        
            return;
        

        try
        
            // Signal cancellation to the executing method
            _stoppingCts.Cancel();
        
        finally
        
            // Wait until the task completes or the stop token triggers
            await Task.WhenAny(_executingTask, Task.Delay(Timeout.Infinite, cancellationToken));
        

    

    public void Dispose()
    
        _stoppingCts.Cancel();
        _timer?.Dispose();
    

【讨论】:

以上是关于如何在 ASP.NET Core 2.1 中的计时器上运行 BackgroundService的主要内容,如果未能解决你的问题,请参考以下文章

共享布局视图不适用于 ASP.NET Core 2.1 中的所有视图

如何在 ASP.NET Core 2.1 中向 Visual Studio 添加新的 Razor 页面模板?

如何在 asp.net core 2.1 中使用 net.tcp 服务

如何在 asp.net core 2.1 中使用自定义消息设置状态代码?

如何在 ASP.NET Core 2.1 中使用自动完成输入引发模式

ViewData 未传递给 ASP.NET Core 2.1 中的布局