netcore后台任务注意事项

开局一张图,故事慢慢编!这是一个后台任务打印时间的德莫,代码如下:

using BackGroundTask;

var builder = WebApplication.CreateBuilder();
builder.Services.AddTransient();
builder.Services.AddHostedService();
builder.Build().Run();

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace BackGroundTask
{
internal class TickerService
{
private event EventHandler Ticked;
public TickerService()
{
Ticked += OnEverySecond;
Ticked += OnEveryFiveSecond;
}
public void OnEverySecond(object? sender,TickerEventArgs args)
{
Console.WriteLine(args.Time.ToLongTimeString());
}
public void OnEveryFiveSecond(object? sender, TickerEventArgs args)
{
if(args.Time.Second %5==0)
Console.WriteLine(args.Time.ToLongTimeString());
}
public void OnTick(TimeOnly time)
{
Ticked?.Invoke(this, new TickerEventArgs(time));
}
}
internal class TickerEventArgs
{
public TimeOnly Time { get; }
public TickerEventArgs(TimeOnly time)
{
Time = time;
}
}
}

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace BackGroundTask
{
internal class TickerBackGroundService : BackgroundService
{
private readonly TickerService _tickerService;
public TickerBackGroundService(TickerService tickerService)
{
_tickerService = tickerService;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
_tickerService.OnTick(TimeOnly.FromDateTime(DateTime.Now));
await Task.Delay(1000,stoppingToken);
}
}
}
}

结果和预期一样,每秒打印一下时间,五秒的时候会重复一次。

代码微调,把打印事件改成打印guid,新增TransientService类:

internal class TransientService
{
public Guid Id { get; }=Guid.NewGuid();
}

微调后代码如下:

using BackGroundTask;

var builder = WebApplication.CreateBuilder();
builder.Services.AddTransient();
builder.Services.AddTransient(); //新增生成guid类
builder.Services.AddHostedService();
builder.Build().Run();

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace BackGroundTask
{
internal class TickerService
{
private event EventHandler Ticked;
private readonly TransientService _transientService; //注入TransientService
public TickerService(TransientService transientService)
{
Ticked += OnEverySecond;
Ticked += OnEveryFiveSecond;
_transientService = transientService;

    }
    public void OnEverySecond(object? sender,TickerEventArgs args)
    {
        Console.WriteLine(\_transientService.Id); //打印guid
    }
    public void OnEveryFiveSecond(object? sender, TickerEventArgs args)
    {
        if(args.Time.Second %5\==0)
        Console.WriteLine(args.Time.ToLongTimeString());
    }
    public void OnTick(TimeOnly time)
    {
        Ticked?.Invoke(this, new TickerEventArgs(time));
    }
}
internal class TickerEventArgs
{
    public TimeOnly Time { get; }
    public TickerEventArgs(TimeOnly time)
    {
        Time \= time;
    }
}

}

TickerBackGroundService类没有做改动,来看看结果:

看似没问题,但是这个guid每次拿到的是一样的,再来看注入的TransientService类,是瞬时的,而且TickerService也是瞬时的。那应该每次会拿到新的对象新的guid才对。那这个后台任务是不是满足不了生命周期控制的要求呢?

问题就出在下面的代码上:

       while (!stoppingToken.IsCancellationRequested)
{
_tickerService.OnTick(TimeOnly.FromDateTime(DateTime.Now));
await Task.Delay(1000,stoppingToken);
}

任务只要不停止,循环会一直下去,所以构造函数注入的类不会被释放,除非程序重启。那么怎么解决这个问题呢,那就是在while里面每次每次循环都创建一个新的对象。那就可以引入ServiceProvider对象。改造后的代码如下:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ConsoleBackGround
{
internal class GlobalService
{
public static IServiceProvider ServiceProvider { get; set; }
}
}

using ConsoleBackGround;

var builder = WebApplication.CreateBuilder();

builder.Services.AddTransient(); //Guid相同
//builder.Services.AddSingleton(); //构造函数使用Guid相同,使用scope对象注入不了,必须用ATransient
//builder.Services.AddScoped(); //构造函数使用Guid相同, 使用scope对象注入不了,必须用ATransient
builder.Services.AddTransient();

GlobalService.ServiceProvider = builder.Services.BuildServiceProvider(); //一定要在注入之后赋值,要不然只会拿到空对象。
builder.Services.AddHostedService();

builder.Build().Run();

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ConsoleBackGround
{
internal class TickerBackGroundService : BackgroundService
{
//private readonly TickerService _tickerService;
//public TickerBackGroundService(TickerService tickerService)
//{
// _tickerService = tickerService;
//}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
//_tickerService.OnTick(TimeOnly.FromDateTime(DateTime.Now)); //guid不会变
using var scope = GlobalService.ServiceProvider.CreateScope();
var _tickerService = scope.ServiceProvider.GetService();
_tickerService?.OnTick(TimeOnly.FromDateTime(DateTime.Now)); //可以保证guid会变
await Task.Delay(1000,stoppingToken);
}
}
}
}

问题出在循环上所以TickerService代码不需要做任何更改。针对方便构造函数注入serviceprovider的情况完全不需要全局的GlobalService,通过构造函数注入的代码如下:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ConsoleBackGround
{
internal class TickerBackGroundService : BackgroundService
{
private readonly IServiceProvider _sp;
public TickerBackGroundService(IServiceProvider sp)
{
_sp = sp;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
////_tickerService.OnTick(TimeOnly.FromDateTime(DateTime.Now)); //guid不会变
using var scope = _sp.CreateScope();
var _tickerService = scope.ServiceProvider.GetService();
_tickerService?.OnTick(TimeOnly.FromDateTime(DateTime.Now)); //可以保证guid会变
await Task.Delay(1000,stoppingToken);
}
}
}
}

运行结果符合预期:

下面看看使用MediatR的代码,也可以达到预期:

using BackGroundMediatR;
using MediatR;

Console.Title = “BackGroundMediatR”;
var builder = WebApplication.CreateBuilder();
//builder.Services.AddSingleton(); //打印相同的guid
builder.Services.AddTransient(); //打印不同的guid
builder.Services.AddMediatR(typeof(Program));

builder.Services.AddHostedService();

builder.Build().Run();

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace BackGroundMediatR
{
internal class TransientService
{
public Guid Id { get; }=Guid.NewGuid();
}
}

using MediatR;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace BackGroundMediatR
{
internal class TimedNotification:INotification
{
public TimeOnly Time { get; set; }
public TimedNotification(TimeOnly time)
{
Time = time;
}
}
}

using MediatR;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace BackGroundMediatR
{
internal class EventSecondHandler : INotificationHandler
{
private readonly TransientService _service;
public EventSecondHandler(TransientService service)
{
_service = service;
}
public Task Handle(TimedNotification notification, CancellationToken cancellationToken)
{
Console.WriteLine(_service.Id);
return Task.CompletedTask;
}
}
}

using MediatR;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace BackGroundMediatR
{
internal class EveryFiveSecondHandler : INotificationHandler
{
public Task Handle(TimedNotification notification, CancellationToken cancellationToken)
{
if(notification.Time.Second % 5==0)
Console.WriteLine(notification.Time.ToLongTimeString());
return Task.CompletedTask;
}
}
}

using MediatR;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace BackGroundMediatR
{
internal class TickerBackGroundService : BackgroundService
{
private readonly IMediator _mediator;
public TickerBackGroundService(IMediator mediator)
{
_mediator = mediator;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
var timeNow = TimeOnly.FromDateTime(DateTime.Now);
await _mediator.Publish(new TimedNotification(timeNow));
await Task.Delay(1000,stoppingToken);
}
}
}
}

执行结果如下:

代码链接:

exercise/Learn_Event at master · liuzhixin405/exercise (github.com)

Over!

Fork me on GitHub