Параллельный.Вызов против ожидаемой / асинхронной производительности задачи

#c# #.net #multithreading #parallel-processing #task

#c# #.net #многопоточность #параллельная обработка #задача

Вопрос:

Наличие базового класса с 2 методами, подобными этому:

 public void ReadAllData(List<Entity> entities){

        //processing
        foreach(Entity e in entities){
            ReadSingleData(entities[i]);
        }   
        //processing
}



  public void ReadSingleData(Entity entity){
        //processing
        db.ReadFromDataBase();
        //processing
    }
  

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

 Action[] actions = new Action[entities.Count);
public void ReadAllData(List<Entity> entities){

        //processing
        for(int i = 0; i< entities.Count;i  ){
            actions[i] = new Action(() => ReadSingleData(entities[i]));
        }   
        //processing
}
ParallelOptions op = new ParallelOptions();
op.MaxDegreeOfParallelism = 6; //number of logical cores
Parallel.Invoke(op, actions);
  

И я использовал методы async / await:

 Task<bool>[] tasks = new Task<bool>[entities.Count];
public void ReadAllData(List<Entity> entities){

        //processing
        for(int i = 0; i< entities.Count;i  ){
            tasks[i] = ReadSingleData(entities[i]);
        }   
        Task.WaitAll(tasks);
        //processing
}

public async Task<bool> ReadSingleData(Entity entity){
    //processing
    await Task.Run(() =>
        db.ReadFromDataBase();
    });
    //processing
    return flag;
}
  

Это просто псевдокод. При реализации задачи я получаю ускорение примерно на 35-40% (примерно на 30 секунд). Мой вопрос в том, как параллельная библиотека работает внутри? Использует ли библиотека задачи, подобные моей второй реализации? Что может быть не так с первой реализацией?
Спасибо.

Комментарии:

1. Вы могли бы взглянуть на реализацию Parallel.Invoke в справочном источнике . «Если количество действий больше 10, мы автоматически используем Parallel.For() для обработки действий, а не стратегию «Задача за действие»

2. Возможно, вы также захотите изучить поток данных TPL .

3. Возможно, стоит также рассмотреть реактивный фреймворк Microsoft. Затем вы можете писать var query = from entity in entities.ToObservable() from flag in Observable.FromAsync(() => ReadSingleData(entity)) select flag; query.ToArray().Wait(); .

Ответ №1:

Разница в том, что Parallel.Invoke в определенных случаях не создает задачу для каждого действия, запланированного к выполнению, и ведет себя аналогично Parallel.ForEach . Более подробно, если количество задач превышает вашу степень параллелизма, которая либо явно указана (как в вашем случае), либо определяется количеством ядер, Parallel.Invoke разбивает действия для вызова на пакеты, чтобы соответствовать требуемой степени параллелизма. Действия в одном пакете выполняются последовательно. Task.Run Напротив, в подавляющем большинстве случаев выполнение расписывается в пуле потоков.

В случае вычислений, ограниченных процессором, Parallel.Invoke логика выигрывает, поскольку она подразумевает эффективное использование доступных ресурсов с небольшим конфликтом. Операции с ограниченным вводом-выводом меняют ситуацию, поскольку теперь у вас много конфликтов ввода-вывода, которые могут остаться нераскрытыми в случае Parallel.Invoke и когда пропускная способность ввода-вывода не может быть полностью использована одновременными Parallel.Invoke действиями, число которых ограничено (другие операции, которые потенциально могут выполняться во время выполнения ввода-вывода, просто ожидают пакетного выполнения). Task.Run в таких сценариях имеет преимущество, поскольку без верхнего предела параллелизма он может покрыть оставшуюся конкуренцию. Вот почему при определенных обстоятельствах это может увеличить производительность. Однако есть недостаток: неконтролируемое применение Task.Run может привести к ситуациям, когда пул потоков перегружен задачами и должен порождать новые потоки для обслуживания входящих задач (когда количество запланированных задач ввода-вывода превышает пропускную способность ввода-вывода), что, в свою очередь, может привести к снижению производительности. Вот почему рекомендуемый способ работы с операциями, ограниченными вводом-выводом, заключается в использовании async/await , поскольку он зависит от внутренних механизмов (таких как порты завершения ввода-вывода), которые обеспечивают оптимальное использование ввода-вывода.