通过计时器写入数据库的后台任务



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

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>();

似乎一切都像示例中那样完成了,但没有向数据库中添加任何内容,事实并非如此?

这是一个非常有趣的问题,可以归结为"如何正确处理异步定时器回调?">

直接的问题是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在《异步指南》的"计时器回调"部分解释了如何正确处理异步计时器回调文章:

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解释了为什么ASP中异步无效是ALWAY BAD。NET核心-不仅异步操作不会被等待,异常还会导致应用程序崩溃。

他还解释了为什么我们不能使用Timer(async state=>DoWorkAsync(state))——这是一个async void委托。

最新更新