Parallel или в один поток? Внезапные результаты

в 11:30, , рубрики: Без рубрики

Исходные данные: Есть простой алгоритм для генерации комбинаций.
Задача: Заставить работать быстрее с минимумом программистчасов
Решение: Parallel, Claster, ThreadTasks
статья может быть дописана

  • исходный алгоритм
    for( int i1cards=0; i1cards< cards.Count(); i1cards++ ) {
            for( int i2cards=0; i2cards< cards.Count(); i2cards++ ) {
              Card[] _cards = new Card[] { cards[i1cards], cards[i2cards] };
              if( result.Exists( pair => pair.Contains(_cards[0]) && pair.Contains(_cards[1]) ) )
                continue;
              result.Add( _cards );
            }
          }
    

    if( result.Exists( pair => pair.Contains(_cards[0]) && pair.Contains(_cards[1]) ) ) — так мы исключаем из результирующего списка сгенеренную комбинацию. (Далее это вынесено за пределы цикла для «чистого тестирования циклов» и соответствующим образом изменено)

  • первое улучшение
    foreach( Card card1 in cards ) {
            foreach( Card card2 in cards ) {
              foreach( Card card3 in cards ) {
                result.Add( new Card[]{card1,card2,card3} );
              }
            }
          }
    
  • второе улучшение
    Parallel.ForEach( cards, ( card1 ) =>
            Parallel.ForEach( cards, ( card2 ) => {
                result.Add( new Card[] { card1, card2 } );
            } ) );
    

    Загрузило процессор на 100% на несколько минут и свыше 10 потоков (число потоков изменялось на протяжении генерации). recult.Count = разные больше 100,000.

  • Добавил в начало функции:
    ParallelOptions po= new ParallelOptions() {
            MaxDegreeOfParallelism = 8,
          };
    

    Это так сказать глобальные настройки для работы Parallel. MaxDegreeOfParallelism — ограничивает максимально допустимым числом потоков (в нашем случае ограничили 8 потоками)
    Загрузка процессора все те же 100% и 10 потоков (с последующим увеличением числа потоков) и отработка около секунды.

  • Partitioner.Create( cards, true ).AsParallel().ForAll( ( card1 ) =>
            Partitioner.Create( cards, true ).AsParallel().ForAll( ( card2 ) =>
              Partitioner.Create( cards, true ).AsParallel().ForAll( ( card3 ) =>
                result.Add( new Card[3] { card1, card2, card3 } )
            ) ));
    

    Partitioner.Create( cards, true ) — заставляет разбивать на блоки. Т.е. если обычный foreach выполняет по одному вхождению в потоке, то в этом случае будет сразу несколько итераций взято в одном потоке.
    Загрузка процессора меньше 5% и потоков выше 50. recult.Count = 113698, 114976. Иногда выскакивает исключилка: «Длина результирующего массива недостаточна» или «Индекс находился вне границ массива.».

  • Partitioner.Create( cards, true ).AsParallel().WithMergeOptions( ParallelMergeOptions.FullyBuffered ).ForAll( ( card1 ) =>
            Partitioner.Create( cards, true ).AsParallel().WithMergeOptions( ParallelMergeOptions.FullyBuffered ).ForAll( ( card2 ) =>
              Partitioner.Create( cards, true ).AsParallel().WithMergeOptions( ParallelMergeOptions.FullyBuffered ).ForAll( ( card3 ) =>
                result.Add( new Card[3] { card1, card2, card3 } )
            ) ));
    

    WithMergeOptions( ParallelMergeOptions.FullyBuffered ) — заставляет при распараллеливании использовать настройку объединения и в нашем случае с режимом FullyBuffered, т.е. полностью буферизовать.
    Число потоков свыше 50, загрузка процессора меньше 5%. recult.Count = 114988, 133495.

  • cards.ForEach( ( card1 ) =>
            cards.ForEach( ( card2 ) =>
              cards.ForEach( ( card3 ) =>
                result.Add( new Card[] { card1, card2, card3 } )
                ) ) );
    

    Отработка мгновенная. Не успел увидеть изменения числа потоков. recult.Count = 140608.
    По идее, должно само распараллеливать вычисления (сейчас уже не могу вспомнить где, но определенные операции вида, Select, Where,… другие запросы разработаны microsoft так, что оно само раскидывает на потоки, а использование AsParallel явно указывает распараллеливать.

  • cards.AsParallel().ForAll( ( card1 ) =>
            cards.AsParallel().ForAll( ( card2 ) =>
              cards.AsParallel().ForAll( ( card3 ) =>
                result.Add( new Card[] { card1, card2, card3 } )
                ) ) );
    

    Процессор меньше 5% и потоков выше 50. recult.Count = 136339, 135416. Иногда выскакивает исключилка: «Длина результирующего массива недостаточна» или «Индекс находился вне границ массива.» (под час частота появления этих ошибок бывает изумительно большая).

Изобретаем свое

будем использовать Thread, Task, WCF (мультипроцесс сеть (бот-нет)… нужно?..

Изюм

Мне, как рядовому программисту, не очень понятно по какому же алгоритму работают эти механизмы от разработчиков microsoft. Там где оно должно работать предсказуемо и просто — работает с какой-то магией.
Все что я вычитал из справки к этим классам не содержит информации о том, что обычный for по конечному числу элементов будет выдавать разные результаты, подчас «Очень» разные, возможно, я нарвался на недокументированные возможности, которые можно использовать как «Генератор случайных чисел».
Я «где-то» читал, что Parallel и PLINQ должны сами рассчитывать оптимальное число потоков в зависимости от ресурсов и ресурсоемкости распараллеливаемого алгоритма, но иногда вычисления занимают через чур много времени, а систему даже не загружают и на 5%.
В итоге я вынужден создавать свой «транспорт», чтобы на нем ехать (это значить кто-то зря ест свой хлеб).

Проблема с исключением из результата

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

Автор: cybermerlin

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js