【问题标题】:Background task of writing to the database by timer定时器写入数据库的后台任务
【发布时间】:2018-11-02 10:06:42
【问题描述】:

如何在后台的计时器上写入数据库。例如,检查邮件并将新信件添加到数据库中。在示例中,我在写入数据库之前简化了代码。

Microsoft 示例中的类名。 录音类本身:

namespace EmailNews.Services
{

internal interface IScopedProcessingService
{
    void DoWork();
}

internal class ScopedProcessingService : IScopedProcessingService
{
    private readonly ApplicationDbContext _context;
    public ScopedProcessingService(ApplicationDbContext context)
    {
        _context = context;
    }

    public void DoWork()
    {
        Mail mail = new Mail();
        mail.Date = DateTime.Now;
        mail.Note = "lala";
        mail.Tema = "lala";
        mail.Email = "lala";
        _context.Add(mail);
        _context.SaveChangesAsync();
    }
}
}

定时器类:

namespace EmailNews.Services
{
#region snippet1
internal class TimedHostedService : IHostedService, IDisposable
{
    private readonly ILogger _logger;
    private Timer _timer;

    public TimedHostedService(IServiceProvider services, ILogger<TimedHostedService> logger)
    {
        Services = services;
        _logger = logger;
    }
    public IServiceProvider Services { get; }

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

        _timer = new Timer(DoWork, null, TimeSpan.Zero,
            TimeSpan.FromMinutes(1));

        return Task.CompletedTask;
    }

    private void DoWork(object state)
    {
        using (var scope = Services.CreateScope())
        {
            var scopedProcessingService =
                scope.ServiceProvider
                    .GetRequiredService<IScopedProcessingService>();

            scopedProcessingService.DoWork();
        }
    }

    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();
    }
}
#endregion
}

启动:

        services.AddHostedService<TimedHostedService>();
        services.AddScoped<IScopedProcessingService, ScopedProcessingService>();

似乎一切都像示例中那样完成,但没有向数据库添加任何内容,不是这样吗?

【问题讨论】:

  • 您是否尝试过调试?定时器会触发吗?是否调用了服务的DoWork?它完成还是抛出?除了调试之外,您至少应该在代码中添加日志记录以记录异常。
  • 这可能是因为您没有 await 您的 SaveChangesAsync 调用,尤其是在该调用完成之前,生成您的 IScopedProcessingService 和相应的 ApplicationDbContext 实例的范围正在处理。
  • Timer 类无法处理异步回调。 DoWork 必须async Task 以允许 SaveChangesAsync 在不阻塞的情况下完成。这意味着您不能从计时器的回调中调用它。您可以将计时器替换为包含Task.Delay() 的循环。
  • 查看 Maarten Balliauw 的文章 Building a scheduled task in ASP.NET Core/Standard 2.0ExecuteAsync 包含一个循环,该循环调用计时器内的任务,然后调用await Task.Delay(...);。这篇文章走得更远,允许定义自定义任务、解析cron调度字符串、一次执行多个任务等
  • David Fowler 创建了一个带有异步陷阱和警告的 Github 存储库。查看Timer Callbacks 部分,了解调用异步回调的正确方法

标签: c# asp.net-core async-await asp.net-core-2.1 asp.net-core-hosted-services


【解决方案1】:

这是一个相当有趣的问题,归结为“您如何正确处理异步计时器回调?”

直接的问题是 SaveChangesAsync 没有得到等待。 DbContext 几乎肯定会在 SaveChangesAsync 有机会运行之前被处理掉。要等待它,DoWork 必须成为 async Task 方法(从不异步无效):

internal interface IScheduledTask
{
    Task DoWorkAsync();
}

internal class MailTask : IScheduledTask
{
    private readonly ApplicationDbContext _context;
    public MailTask(ApplicationDbContext context)
    {
        _context = context;
    }

    public async Task DoWorkAsync()
    {
        var mail = new Mail 
                   { Date = DateTime.Now,
                     Note = "lala",
                     Tema = "lala",
                     Email = "lala" };
        _context.Add(mail);
        await _context.SaveChangesAsync();
    }
}

现在的问题是如何从定时器回调中调用DoWorkAsync。如果我们不等待就直接调用它,我们将遇到一开始遇到的相同问题。计时器回调无法处理返回 Task 的方法。我们也不能做到async void,因为这会导致同样的问题——该方法将在任何异步操作有机会完成之前返回。

David Fowler 在其Async GuidanceTimer Callbacks 部分解释了如何正确处理异步计时器回调 文章:

private readonly Timer _timer;
private readonly HttpClient _client;

public Pinger(HttpClient client)
{
    _client = new HttpClient();
    _timer = new Timer(Heartbeat, null, 1000, 1000);
}

public void Heartbeat(object state)
{
    // Discard the result
    _ = DoAsyncPing();
}

private async Task DoAsyncPing()
{
    await _client.GetAsync("http://mybackend/api/ping");
}

实际的方法应该是async Task,但返回的任务只需分配,而不是等待,以便它正常工作。

将其应用于问题会导致这样的结果:

public Task StartAsync(CancellationToken cancellationToken)
{
    ...
    _timer = new Timer(HeartBeat, null, TimeSpan.Zero,
        TimeSpan.FromMinutes(1));

    return Task.CompletedTask;
}

private void Heartbeat(object state)
{
    _ = DoWorkAsync();
}


private async Task DoWorkAsync()
{
    using (var scope = Services.CreateScope())
    {
        var schedTask = scope.ServiceProvider
                             .GetRequiredService<IScheduledTask>();

        await schedTask.DoWorkAsync();
    }
}

David Fowler 解释了为什么 async void 是 ALWAY BAD in ASP.NET Core - 不仅不等待异步操作,异常还会导致应用程序崩溃。

他还解释了为什么 we can't use Timer(async state=&gt;DoWorkAsync(state)) - 那是 async void 代表。

【讨论】:

    猜你喜欢
    • 2022-01-16
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-04-20
    • 2017-02-05
    • 2021-12-27
    • 1970-01-01
    相关资源
    最近更新 更多