Я пытаюсь реализовать автономный веб-сервис с использованием asp.net core 2.1 и застрял с проблемой реализации фоновых задач длительного выполнения.

Из-за высокой загрузки процессора и затрат времени на каждый метод ProcessSingle (в фрагменте кода ниже) я хотел бы ограничить количество выполняемых одновременно задач. Но как я вижу, все задачи в Parallel.ForEach запускаются почти сразу, несмотря на то, что я установил MaxDegreeOfParallelism = 3

Мой код (это упрощенная версия):

public static async Task<int> Work()
{
    var id = await CreateIdInDB() // async create record in DB

    // run background task, don't wait when it finishes
    Task.Factory.StartNew(async () => {
        Parallel.ForEach(
            listOfData,
            new ParallelOptions { CancellationToken = token, MaxDegreeOfParallelism = 3 },
            async x => await ProcessSingle(x));
    });

    // return created id immediately
    return id;
}

public static async Task ProcessSingle(MyInputData inputData)
{
    var dbData = await GetDataFromDb(); // get data from DB async using Dapper
    // some lasting processing (sync)
    await SaveDataToDb(); // async save processed data to DB using Dapper
}

Если я правильно понимаю, проблема в async x => await ProcessSingle(x) внутри Parallel.ForEach, не так ли?

Не мог бы кто-нибудь описать, как это должно быть правильно реализовано?

< Сильный > Обновление

Из-за некоторой двусмысленности в моем вопросе необходимо остановиться на основных аспектах:

  1. Метод ProcessSingle состоит из трех частей:

    • получение данных из БД асинхронно

    • выполнять длительные математические вычисления с высокой загрузкой процессора

    • сохранить результаты в асинхронной базе данных

  2. Проблема состоит из двух отдельных:

    • Как уменьшить загрузку ЦП (например, выполняя не более трех математических вычислений одновременно)?

    • Как сохранить структуру метода ProcessSingle - сохранить их асинхронными из-за асинхронных вызовов БД.

Надеюсь, теперь будет более ясно.

Постскриптум Подходящий ответ уже дан, работает (особенно спасибо @MatrixTai). Это обновление было написано для общего разъяснения.

-2
user1820686 14 Сен 2018 в 09:16

2 ответа

Лучший ответ

< Сильный > Обновление

Как я только что заметил, вы упомянули в комментарии, проблема вызвана математическим расчетом.

Лучше будет разделить часть расчета и обновления БД.

Для расчетной части используйте Parallel.ForEach(), чтобы оптимизировать вашу работу и вы можете контролировать количество потоков.

И только после того, как все эти задачи выполнены. Используйте async-await для обновления ваших данных в БД без упомянутого мной SemaphoreSlim.

public static async Task<int> Work()
{
    var id = await CreateIdInDB() // async create record in DB

    // run background task, don't wait when it finishes
    Task.Run(async () => {

        //Calculation Part
        ConcurrentBag<int> data = new ConcurrentBag<int>();
        Parallel.ForEach(
            listOfData,
            new ParallelOptions { CancellationToken = token, MaxDegreeOfParallelism = 3 },
            x => {ConcurrentBag.Add(calculationPart(x))});

        //Update DB part
        int[] data_arr = data.ToArray();
        List<Task> worker = new List<Task>();
        foreach (var i in data_arr)
        {
            worker.Add(DBPart(x));
        }
        await Task.WhenAll(worker);
    });

    // return created id immediately
    return id;
}

Конечно, все они начинаются вместе, поскольку вы используете async-await в Parallel.forEach.

Сначала прочтите об этом вопросе как для 1-го, так и для 2-го ответа. Объединять эти два понятия бессмысленно.

Фактически async-await максимизирует использование доступного потока, поэтому просто используйте его.

public static async Task<int> Work()
{
    var id = await CreateIdInDB() // async create record in DB

    // run background task, don't wait when it finishes
    Task.Run(async () => {
        List<Task> worker = new List<Task>();
        foreach (var i in listOfData)
        {
            worker.Add(ProcessSingle(x));
        }
        await Task.WhenAll(worker);
    });

    // return created id immediately
    return id;
}

Но тогда возникает еще одна проблема: в этом случае эти задачи по-прежнему запускаются вместе, потребляя ресурсы вашего процессора.

Чтобы этого избежать, используйте SemaphoreSlim

public static async Task<int> Work()
{
    var id = await CreateIdInDB() // async create record in DB

    // run background task, don't wait when it finishes
    Task.Run(async () => {
        List<Task> worker = new List<Task>();
        //To limit the number of Task started.
        var throttler = new SemaphoreSlim(initialCount: 20);
        foreach (var i in listOfData)
        {
            await throttler.WaitAsync();
            worker.Add(Task.Run(async () =>
            {
                await ProcessSingle(x);
                throttler.Release();
            }
            ));
        }
        await Task.WhenAll(worker);
    });

    // return created id immediately
    return id;
}

Подробнее Как ограничить количество одновременных асинхронных операций ввода-вывода O операции?.

Кроме того, не используйте Task.Factory.StartNew(), когда простого Task.Run() достаточно для выполнения нужной вам работы, прочтите этот отличный статья Стивена Клири.

1
MT-FreeHongKong 14 Сен 2018 в 07:52

Если вы более знакомы с «традиционной» концепцией параллельной обработки, перепишите свой метод ProcessSingle () следующим образом:

public static void ProcessSingle(MyInputData inputData)
{
    var dbData = GetDataFromDb(); // get data from DB async using Dapper
    // some lasting processing (sync)
    SaveDataToDb(); // async save processed data to DB using Dapper
}

Конечно, вы также желательно изменить метод Work () аналогичным образом.

0
PMF 14 Сен 2018 в 07:02