Параллельные фоновые задачи в очереди с размещенными службами в ASP.NET Core

Я выполняю некоторые тесты с новыми фоновыми задачами с размещенными службами в функции ASP.NET Core, представленной в версии 2.1, в частности, с фоновыми задачами в очереди, и мне на ум пришел вопрос о параллелизме.

В настоящее время я строго следую учебному пособию, предоставленному Microsoft, и при попытке смоделировать рабочую нагрузку с несколькими запросами, сделанными одним и тем же пользователем для постановки задач в очередь, я заметил, что все рабочие элементы выполняются по порядку, поэтому нет параллелизма.

Мой вопрос в том, ожидается ли такое поведение? И если да, то можно ли запустить и забыть, чтобы сделать выполнение запроса параллельным, вместо того, чтобы ждать завершения рабочего элемента?

Я безуспешно искал пару дней об этом конкретном сценарии, поэтому, если у кого-то есть какое-либо руководство или примеры, я был бы очень рад.

Изменить. Код из руководства довольно длинный, поэтому ссылка на него: https://docs.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)}.");
            }
        }
    }

    ...
}

Суть вопроса заключается в том, чтобы узнать, может ли кто-нибудь поделиться знаниями о том, как использовать эту конкретную технологию для одновременного выполнения нескольких рабочих элементов, поскольку сервер может справиться с этой рабочей нагрузкой.

Я попробовал метод «выстрелил и забыл» при выполнении рабочего элемента, и он работал так, как я предполагал, несколько задач выполнялись параллельно одновременно, я просто не уверен, что это нормальная практика, или если есть лучший или правильный способ справиться с этой ситуацией.


person marceloatg    schedule 15.07.2018    source источник
comment
Если вы внедрили его после этот пример, поэтому неудивительно, что задачи выполняются по порядку, потому что в этом весь смысл модели. Метод BackgroundProceessing всегда ожидает следующей задачи. Это означает, что задачи выполняются параллельно (с веб-сервером), но по порядку.   -  person a-ctor    schedule 15.07.2018
comment
Итак, вы говорите, что после развертывания на сервере с несколькими запросами, поступающими от разных пользователей, эти задачи будут выполняться параллельно? И поскольку я новичок в переполнении стека, что означает -1 в моем вопросе?   -  person marceloatg    schedule 15.07.2018
comment
ваши вопросы можно было бы улучшить, показав нам, что вы сделали. Фрагмент кода того, что вы сделали, было бы здорово. Итак, я предполагаю, что вы использовали пример кода, который я связал?   -  person a-ctor    schedule 15.07.2018
comment
Код из примера выполняется параллельно (параллельно потокам, обрабатывающим веб-запросы), но одновременно выполняется только одна задача. Это означает, что добавление двух элементов в размещенную службу будет выполнять их в том порядке, в котором они были добавлены, но они будут выполняться параллельно с обработкой веб-запросов.   -  person a-ctor    schedule 15.07.2018
comment
да, я предположил, что это достаточно конкретно, поскольку это единственная официальная документация по этой конкретной теме для этой конкретной технологии, спасибо за разъяснения. Но самое главное, можете ли вы подтвердить, что после развертывания на сервере с несколькими запросами, поступающими от разных пользователей, эти задачи будут выполняться параллельно? В этом весь смысл вопроса, и требуется некоторое время, пока я не смогу найти сервер и развернуть код, а затем запустить тесты (что, кстати, я делаю прямо сейчас с бесплатным уровнем AWS).   -  person marceloatg    schedule 15.07.2018
comment
ладно, я думаю, я не очень хорошо выражаюсь. Что я действительно хочу знать, так это то, есть ли способ одновременного выполнения нескольких задач, поскольку сервер может с этим справиться. Я попробовал метод «выстрелил и забыл», и он сработал так, как я предполагал, я просто не уверен, что это нормальная практика.   -  person marceloatg    schedule 15.07.2018


Ответы (2)


Размещенный вами код выполняет элементы очереди по порядку, по одному, а также параллельно с веб-сервером. IHostedService выполняется для каждого определения параллельно с веб-сервером. Эта статья содержит хороший обзор.

Рассмотрим следующий пример:

_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

Чтобы ввести параллелизм, мы должны изменить реализацию метода BackgroundProceessing в методе QueuedHostedService.


Вот пример реализации, которая позволяет выполнять две задачи параллельно:

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

edit: Можно ли в производственной среде выполнять код таким образом, не дожидаясь его?

Я думаю, что причина, по которой у большинства разработчиков возникают проблемы с принципом «выстрелил и забыл», заключается в том, что им часто злоупотребляют.

Когда вы выполняете Task с помощью «выстрелил и забыл», вы в основном говорите мне, что вас не волнует результат этой функции. Вам все равно, успешно ли он завершится, будет ли он отменен или возникнет исключение. Но для большинства Task вам действительно важен результат.

  • Вы хотите убедиться, что запись в базу данных прошла
  • Вы хотите убедиться, что запись журнала записана на жесткий диск
  • Вы хотите убедиться, что сетевой пакет отправлен получателю

А если вас волнует результат Task, то метод "выстрелил-забыл" - неправильный метод.

Вот и все, на мой взгляд. Сложность заключается в том, чтобы найти Task, где вас действительно не волнует результат Task.

person a-ctor    schedule 15.07.2018
comment
Спасибо за ваш ответ и за ваше время, этот подход - это в основном то, что я пробовал с огнем и забыл для рабочего элемента, единственная разница в вашем коде - это SemaphoreSlim, ограничивающий параллельную работу. Итак, мы возвращаемся к вопросу (и, пожалуйста, это честный вопрос, я просто пытаюсь узнать больше об этой теме), нормально ли в производственной среде выполнять код таким образом, не дожидаясь его? Я спрашиваю об этом, потому что в каждом связанном вопросе в stackoverflow люди всегда осуждают эту практику и не знают, имеет ли она влияние на реальный мир в этом случае. - person marceloatg; 15.07.2018
comment
@marceloatg смотрите мою правку. Надеюсь, это ответит на ваш вопрос :) - person a-ctor; 15.07.2018
comment
да, это тот вопрос, на который люди обычно не отвечают прямо по делу, как вы это сделали в своем редактировании, поэтому большое спасибо за объяснение. - person marceloatg; 15.07.2018

Вы можете добавить QueuedHostedService один или два раза для каждого процессора в машине.

Что-то вроде этого:

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

Вы можете скрыть это в методе расширения и сделать уровень параллелизма настраиваемым, чтобы все было чисто.

person alex.pino    schedule 22.11.2018
comment
Привет, Алекс, ты использовал этот подход в производственной среде? это надежно? Я сделал что-то подобное, но я не мог найти никакой документации об этом. - person Gabriel Cerutti; 01.05.2019
comment
Да, у меня есть. Я также просмотрел исходный код на github, чтобы убедиться, что он работает. - person alex.pino; 15.07.2019
comment
По какой-то причине это не работает для меня. Принятый ответ. - person Olivier MATROT; 18.05.2020
comment
Подход с «ProcessorCount» не будет работать в случае хостинга kubernetes/docker. Более того, иногда вам может понадобиться 20 сервисов Queded даже на двухпроцессорной машине (в случае, если каждый сервис обрабатывает вызовы к внешним ресурсам асинхронно). - person Maciej Pszczolinski; 20.12.2020