C# 技巧 :建立后台服务的简单方法

作者:微信公众号:【架构师老卢】
7-4 18:9
43

概述:托管服务是一个具有后台任务逻辑的类,可以在我们的 ASP.NET 服务器的后台运行。这有 2 种变体:1. IHostedSevice:通常降级为短期运行任务。该接口为主机管理的对象定义了两种方法:StartAsync(取消令牌)StopAsync(取消令牌)2. BackgroundService:作为长时间运行或并发任务的扩展引入。该接口为主机管理的对象定义了一种方法:ExecuteAsync(CancellationToken, stoppingToken)两者都需要使用 AddHostedService 方法进行注册。builder.Services.AddHostedServiceM

托管服务是一个具有后台任务逻辑的类,可以在我们的 ASP.NET 服务器的后台运行。

这有 2 种变体:
1. IHostedSevice:通常降级为短期运行任务。该接口为主机管理的对象定义了两种方法:

  • StartAsync(取消令牌)
  • StopAsync(取消令牌)

2. BackgroundService:作为长时间运行或并发任务的扩展引入。该接口为主机管理的对象定义了一种方法:

  • ExecuteAsync(CancellationToken, stoppingToken)

两者都需要使用 AddHostedService 方法进行注册。

builder.Services.AddHostedService<MyBackgroundService>();

让我们看一个更具体的例子

在我的 API 中,需要执行 2 个长时间运行的任务。

public interface IMonitorService  
{  
    Task Monitor(MonitorSettings settings);  
}  
  
public interface IImportingService  
{  
    Task Import(ImportingSettings settings);  
}

这些任务的实现在这里并不重要,只需假设它们执行一些网络调用即可。
我们当然会在 ASP.Net Program.cs文件中注册它们。

builder.Services.AddTransient<IMonitorService, MonitorService>();  
builder.Services.AddTransient<IImportingService, ImportingService>();

现在,我们需要一个关于如何启动监控或导入任务的后台服务指令的通行证,我们希望确保任务将按照收到的顺序执行。

听起来我们需要使用一个队列,让我们创建一个类来容纳它。(我正在使用 ConcurrentQueue,因为它是线程安全的)

public class TasksToRun  
{  
    private readonly ConcurrentQueue<TaskSettings> _tasks = new();  
  
    public TasksToRun() => _tasks = new ConcurrentQueue<TaskSettings>();  
  
    public void Enqueue(TaskSettings settings) => _tasks.Enqueue(settings);  
  
    public TaskSettings? Dequeue()  
    {  
        var hasTasks = _tasks.TryDequeue(out var settings);  
        return hasTasks ? settings : null;  
    }  
}

我们还需要将其注册为单例,以便我们可以在项目的任何类中获取 DI 的相同实例。

builder.Services.AddSingleton<TasksToRun, TasksToRun>();

我们存储在 ConcurrentQueue 中的 TaskSettings 类如下所示:

public class TaskSettings  
{  
    public MonitorSettings? MonitorSettings { get; set; }  
    public ImportingSettings? ImportingSettings { get; set; }  
  
    public static TaskSettings FromMonitorSettings  
                                (MonitorSettings monitorSettings)  
    {  
        return new TaskSettings { MonitorSettings = monitorSettings };  
    }  
  
    public static TaskSettings FromImporterSettings  
                                (ImportingSettings importingSettings)  
    {  
        return new TaskSettings { ImportingSettings = importingSettings };  
    }  
}

它只是 ImportingSettingsMonitorSettings 的容器,我们使用单个类来存储两个设置对象,因此我们可以轻松地使用单个队列。

现在,我们需要创建一个控制器来将任务添加到我们的队列中。

 [ApiController]
 [Route("[controller]")]
 public class TasksController : ControllerBase
 {
     private readonly TasksToRun _tasksToRun;

     public TasksController(TasksToRun tasksToRun)
     {
         _tasksToRun = tasksToRun;
     }

     [HttpGet("monitor")]
     public Task<string> Monitor()
     {
         _tasksToRun.Enqueue(HostingSettings.FromMonitorSettings(new MonitorSettings
         {
             ApiKey = "xxxxxx"
         }));
         return Task.FromResult("monitoring");
     }

     [HttpGet("import")]
     public Task<string> Import()
     {
         _tasksToRun.Enqueue(HostingSettings.FromImporterSettings(new ImportingSettings
         {
             Source = "http://someData.com",
             Count = 100
         }));
         return Task.FromResult("importing");
     }
 }

如您所见,我们从 DI 获取 taskToRun,并在需要后台服务执行某些操作时将其添加到队列中。我们可以向用户返回响应,而无需等待任务完成。

现在,让我们看看后台服务

public class MainBackgroundTaskService : BackgroundService
{
    private readonly TasksToRun _tasks;
    private readonly IMonitorService _monitorService;
    private readonly IImportingService _importingService;

    //get number of seconds from config
    private readonly TimeSpan _timeSpan = TimeSpan.FromSeconds(1);
    public MainBackgroundTaskService(TasksToRun tasks,
        IMonitorService monitorService,
        IImportingService importingService)
    {
        _tasks = tasks;
        _monitorService = monitorService;
        _importingService = importingService;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        using PeriodicTimer timer = new(_timeSpan);
        while (!stoppingToken.IsCancellationRequested &&
               await timer.WaitForNextTickAsync(stoppingToken))
        {
           var taskToRun = _tasks.Dequeue();
           if (taskToRun != null)
            {
                  var taskToRun = _tasks.Dequeue();
                  if (taskToRun == null) continue;

                  //call the relevant service based on what 
                  // settings object in not NULL
                  if (taskToRun?.ImportingSettings != null)
                  {
                      await _importingService .Monitor(taskToRun.ImportingSettings);
                  }
                
                  if (taskToRun?.MonitorSettings != null)
                  {
                      await _monitorService.Import(taskToRun.MonitorSettings);
                  }
            }
        }
    }
}

不要忘记我们需要注册它

builder.Services.AddHostedService<MainBackgroundTaskService>();

该服务非常简单,我们从 DI 中获取要运行的 taskToRun 对象和服务实现,并定义一个 TimeSpan 来定义我们想要对队列进行采样的频率。

ExecuteAsync 方法中,我们定义了一个 PeriodicTimer,以帮助我们等待下次想要从队列中读取的时间(使用_计时器。WaitForNextTickAsync_)。

然后我们只需要从队列中读取,如果我们得到一个任务,检查哪个设置对象不是空来调用相关的服务。

我们等待每个任务,因此请确保他们一次执行一项任务,但我们不必这样做。

if (taskToRun != null)
{
      var taskToRun = _tasks.Dequeue();
      if (taskToRun == null) continue;

      //call the relevant service based on what 
      // settings object in not NULL
      if (taskToRun?.ImportingSettings != null)
      {
          //start to excute but dont wait for completion
          _importingService .Monitor(taskToRun.ImportingSettings);
      }
    
      if (taskToRun?.MonitorSettings != null)
      {
         //start to excute but dont wait for completion
         _monitorService.Import(taskToRun.MonitorSettings);
      }
}

我们还可以并行执行任务

while (!stoppingToken.IsCancellationRequested &&
      await timer.WaitForNextTickAsync(stoppingToken))
{
   var settingsList= new List<TaskSettings>();

   while (!_tasks.IsEmpty)
   {
       settingsList.Add(_tasks.Dequeue());
   }

   var tasksToExecute = settingsList.Select(t =>
       t?.ImportingSettings != null
       ? _importingService .Monitor(taskToRun.ImportingSettings)
           : (t?.MonitorSettings != null ?
                _monitorService.Import(taskToRun.MonitorSettings); : 
                Task.CompletedTask));

   await Task.WhenAll(tasksToExecute);
}

一旦我们有了一个从任务队列中分离出来的机制,我们就可以以我们需要的任何方式执行它们。

如您所知,后台和托管服务是一种非常简单灵活的方式,可以在幕后执行操作。

源代码获取:公众号回复消息【code:43258

相关代码下载地址
重要提示!:取消关注公众号后将无法再启用回复功能,不支持解封!
第一步:微信扫码关键公众号“架构师老卢”
第二步:在公众号聊天框发送code:43258,如:code:43258 获取下载地址
第三步:恭喜你,快去下载你想要的资源吧
阅读排行