netcore後臺任務注意事項

星仔007發表於2022-03-27

開局一張圖,故事慢慢編!這是一個後臺任務列印時間的德莫,程式碼如下:

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!

 

相關文章