使用ASP.NET Core中的托管服务进行并行排队后台任务

17

我正在使用ASP.NET Core 2.1中新增的托管服务和后台任务功能进行一些测试,更具体地说是测试队列后台任务,并且关于并行性的问题浮现了出来。

目前我严格按照Microsoft提供的教程操作,在尝试模拟同一用户多次请求以排队任务的方式执行工作时,我发现所有的workItems都是按顺序执行,因此没有并行性。

我的问题是,这种行为是否符合预期?如果符合预期,为了使请求执行并行,是不是可以使用fire and forget而不是等待workItem完成?

我已经搜索了几天,没有找到关于这种情况的指南或示例,所以如果有人能够提供任何指导或示例,我将不胜感激。

编辑:由于来自教程的代码相当长,因此链接为https://learn.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services?view=aspnetcore-2.1#queued-background-tasks

执行工作项的方法如下:

public class QueuedHostedService : IHostedService
{
    ...

    public Task StartAsync(CancellationToken cancellationToken)
    {
        _logger.LogInformation("Queued Hosted Service is starting.");

        _backgroundTask = Task.Run(BackgroundProceessing);

        return Task.CompletedTask;
    }

    private async Task BackgroundProceessing()
    {
        while (!_shutdown.IsCancellationRequested)
        {
            var workItem = 
                await TaskQueue.DequeueAsync(_shutdown.Token);

            try
            {
                await workItem(_shutdown.Token);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, 
                    $"Error occurred executing {nameof(workItem)}.");
            }
        }
    }

    ...
}

这个问题的主要点是想知道是否有人能够分享如何使用这个特定技术同时执行多个工作项,因为服务器可以处理这个工作负载。

当我执行工作项时,我尝试了“fire and forget”方法,它按照我打算的方式工作了,多个任务同时并行执行,但我不确定这是否是一个好做法,或者是否有更好或更适当的处理这种情况的方法。


如果您按照示例实现它,那么任务按顺序执行就不足为奇了,因为这正是该模型的全部意义。BackgroundProceessing方法始终等待下一个任务。这意味着任务在并行(对于Web服务器)但按顺序处理。 - a-ctor
所以,你的意思是一旦部署到服务器上,多个用户发出的请求将会并行执行? 因为我是Stack Overflow的新手,那么我的问题上的-1是什么意思? - marceloatg
你的问题可以通过展示你所做的来改进。一个你所做的代码片段会很好。所以我假设你使用了我提供的示例代码? - a-ctor
示例中的代码是并行运行的(与处理Web请求的线程并行),但同一时间只执行一个任务。这意味着将两个项目添加到托管服务中将按照它们添加的顺序执行,但它们将与Web请求处理并行执行。 - a-ctor
1
好的,我想我表达得不是很清楚。我真正想知道的是是否有一种同时执行多个任务的方法,因为服务器可以处理这些任务。我尝试了“点火并忘记”方法,并且它按照我预期的方式工作,但我不确定这是否是一个好的做法。 - marceloatg
显示剩余2条评论
2个回答

14

您发布的代码按顺序执行排队的项目,但同时也与Web服务器并行执行。根据定义,每个IHostedService都在与Web服务器并行运行。这篇文章提供了很好的概述。

考虑以下示例:

_logger.LogInformation ("Before()");
for (var i = 0; i < 10; i++)
{
  var j = i;
  _backgroundTaskQueue.QueueBackgroundWorkItem (async token =>
  {
    var random = new Random();
    await Task.Delay (random.Next (50, 1000), token);
    _logger.LogInformation ($"Event {j}");
  });
}
_logger.LogInformation ("After()");

我们添加了十个任务,这些任务会等待随机的时间。如果您将代码放在控制器方法中,即使控制器方法返回后,事件仍将被记录。但是每个项目将按顺序执行,以便输出看起来像这样:

Event 1
Event 2
...
Event 9
Event 10
为了引入并行处理,我们需要更改QueuedHostedServiceBackgroundProceessing方法的实现。
这里是一个示例实现,允许同时执行两个任务:
private async Task BackgroundProceessing()
{
  var semaphore = new SemaphoreSlim (2);

  void HandleTask(Task task)
  {
    semaphore.Release();
  }

  while (!_shutdown.IsCancellationRequested)
  {
    await semaphore.WaitAsync();
    var item = await TaskQueue.DequeueAsync(_shutdown.Token);

    var task = item (_shutdown.Token);
    task.ContinueWith (HandleTask);
  }
}
使用这个实现后,事件的顺序不再按顺序记录,因为每个任务都会等待随机时间。因此输出可能是:
使用这种实现后,记录的事件顺序不再有序,因为每个任务都会等待一段随机时间。因此输出可能如下:
Event 0
Event 1
Event 2
Event 3
Event 4
Event 5
Event 7
Event 6
Event 9
Event 8

编辑:在生产环境中执行代码而无需等待它是否可以?

我认为大多数开发人员对“fire-and-forget”有问题的原因是它经常被错误使用。

当您使用fire-and-forget执行一个任务时,您基本上告诉我您不关心此函数的结果。您并不关心它是否成功退出,是否被取消或是否引发异常。但是对于大多数任务,您确实关心结果。

  • 您确实希望确保数据库写入完成
  • 您确实希望确保将日志条目写入硬盘
  • 您确实希望确保将网络数据包发送到接收方

如果您关心任务的结果,则fire-and-forget是错误的方法

这就是我的想法。难的部分是找到你真正不关心任务结果的任务



谢谢您的答案和时间,这种方法基本上就是我尝试的fire and forget方式处理工作项,您代码中唯一的区别是使用了SemaphoreSlim来限制并发工作。那么我们回到问题上来(请相信,这是一个诚实的问题,我只是想更多地了解这个话题),在生产环境中以这种方式执行代码是否可以?之所以问这个问题是因为在stackoverflow的每个相关问题中,人们都谴责这种做法,并不知道它对现实世界有什么影响。 - marceloatg
@marceloatg 看看我的编辑。希望它回答了你的问题 :) - a-ctor
是的,这种问题通常人们不会像你在编辑中那样直截了当地回答,所以非常感谢你的解释。 - marceloatg

3

您可以为每个CPU添加一次或两次QueuedHostedService。

因此,可以这样做:

for (var i=0;i<Environment.ProcessorCount;++i)
{
    services.AddHostedService<QueuedHostedService>();
}

您可以将此功能隐藏在扩展方法中,并使并发级别可配置,以保持代码的整洁。


嗨,Alex,你在生产环境中使用过这种方法吗?它可靠吗?我做了类似的事情,但是找不到任何关于它的文档。 - Gabriel Cerutti
是的,我有。我还查看了Github上的源代码以确保它能够正常工作。 - alex.pino
出于某种原因,这对我来说没有用。被接受的答案是。 - anon
使用“ProcessorCount”方法在Kubernetes/Docker主机上可能会失败。此外,有时您可能希望在仅有2个CPU的机器上运行20个排队服务(如果每个服务以异步方式处理对外部资源的调用)。 - Maciej Pszczolinski
多次调用可能无法正常工作。不过有一个解决方法。https://github.com/dotnet/runtime/issues/38751 - benmccallum

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接