netcore后台任务注意事项

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

using BackGroundTask;

var builder = WebApplication.CreateBuilder();
builder.Services.AddTransient<TickerService>();
builder.Services.AddHostedService<TickerBackGroundService>();
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<TickerEventArgs> 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<TickerService>();
builder.Services.AddTransient<TransientService>(); //新增生成guid类
builder.Services.AddHostedService<TickerBackGroundService>();
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<TickerEventArgs> 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<TransientService>();  //Guid相同
//builder.Services.AddSingleton<TransientService>(); //构造函数使用Guid相同,使用scope对象注入不了,必须用ATransient
//builder.Services.AddScoped<TransientService>(); //构造函数使用Guid相同, 使用scope对象注入不了,必须用ATransient
builder.Services.AddTransient<TickerService>();

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

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>();
                _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>();
                _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<TransientService>();  //打印相同的guid
builder.Services.AddTransient<TransientService>();  //打印不同的guid
builder.Services.AddMediatR(typeof(Program));

builder.Services.AddHostedService<TickerBackGroundService>();

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<TimedNotification>
    {
        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<TimedNotification>
    {
        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!