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