Урок 4. Прерывание параллельных циклов


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

Раннее завершение цикла

Все стандартные циклы, предоставляемые C#, а именно циклы for, foreach и while, дают вам возможность досрочно выйти из цикла с помощью команды break. При обнаружении цикл немедленно останавливается, все оставшиеся итерации отменяются, и программа продолжает выполнение команды, следующей за циклом. Это полезно, когда продолжать цикл неэффективно. Например, если вы просматриваете набор значений в поисках определенного элемента, вы можете выйти из цикла, когда элемент будет найден.

Следующий цикл for продолжается до тех пор, пока не будет найдено значение, которое больше или равно 15. Итерации с 16 по 20 отменяются break.

for (int i = 1; i <= 20; i++)
{
    Console.Write(i + " ");
    if (i >= 15)
    {
        Console.WriteLine("Break on {0}", i);
        break;
    }
}
 
/* 
 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 Остановка на 15
 
*/
Досрочное прекращение параллельных циклов

Когда вы работаете с параллельными циклами, не так просто выйти рано. Поскольку несколько итераций цикла могут выполняться параллельно, выход из одной итерации на одном процессоре должен быть синхронизирован с другими итерациями, выполняемыми на других ядрах. Если бы параллельные операции были немедленно остановлены, возможно, что данные остались бы в несогласованном состоянии, поэтому другие итерации всегда будут продолжать выполняться. Это означает, что раннее завершение параллельного цикла обычно дает разные результаты для выхода из последовательной версии.

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

Parallel.For(1, 20, i =>
{
    Console.WriteLine(i);
    if (i >= 15)
    {
        Console.WriteLine("Break on {0}", i);
        break;
    }
});
Команда break недоступна, поскольку, как упоминалось ранее в учебнике, параллельные циклы предоставляются статическими методами класса Parallel, а не как часть языка C#. Команда break-это ключевое слово C#, которое работает только с циклами C#. Для координации итераций параллельных циклов, включая выход из них по мере необходимости, мы должны использовать экземпляр класса ParallelLoopState.

Класс ParallelLoopState позволяет итерациям параллельных циклов For и ForEach взаимодействовать друг с другом. Вы не можете создать экземпляр класса напрямую. Вместо этого в лямбда-выражение добавляется второй параметр, определяющий тело цикла. Метод loop автоматически создает объект состояния цикла и предоставляет вам доступ к нему через второй параметр. Затем вы можете использовать методы объекта ParallelLoopState для выхода из циклов или получения информации, заданной в других итерациях.

Использование ParallelLoopState.Break

Первый из методов ParallelLoopState - Break. Он похож на оператор break последовательных циклов. Приведенный ниже код показывает метод в действии. Здесь мы воссоздали цикл из приведенного выше последовательного кода. Параллельный цикл декомпозирует итерации в соответствии с количеством доступных ядер. Затем он обрабатывает каждую итерацию, проверяя, является ли значение управления циклом больше или равно пятнадцати. Когда такое значение найдено, выполняется метод Break. Обратите внимание, что переменная ParallelLoopState с именем "pls" не создается непосредственно.

Parallel.For(1, 20, (i, pls) =>
{
    Console.Write(i + " ");
    if (i >= 15)
    {
        Console.WriteLine("Break on {0}", i);
        pls.Break();
    }
});
 
/* 
 
1 2 3 4 5 6 7 8 9 10 11 12 19 прерывание на 19
13 14 15 Перерывание на 15
 
*/
Результаты цикла интересны. В комментарии к образцу кода вы можете увидеть набор результатов, полученных с помощью двухъядерного процессора. Вы можете видеть, что метод Break был выполнен дважды, чего вы, возможно, не ожидали. Кроме того, были выведены значения выше пятнадцати, что не произошло бы с последовательной версией цикла.

Одно ядро начало работать с итерации, где переменная управления циклом имела значение единицы. Он продолжал обрабатывать итерации 2, 3 и так далее. Другое ядро началось с итерации 19. Когда оператор if проверил значение, оказалось, что оно больше или равно пятнадцати, поэтому был вызван метод Break. Однако цикл не прекратил выполнение в этот момент.

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

Причина такого поведения заключается в том, что метод Break пытается имитировать команду break последовательных циклов. В частности, он пытается гарантировать, что все итерации, которые были бы выполнены последовательно, будут обработаны в параллельном цикле. Когда какие-либо вызовы ядра прерываются, номер итерации записывается в объект ParallelLoopState. Он становится номером последней итерации, которая может быть выполнена. Другие потоки выполнения будут продолжать работать до тех пор, пока они не достигнут этого номера итерации или не столкнутся с другим оператором Break, который еще больше снизит это число.

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

LowestBreakIteration

Мы можем увидеть, как вызовы метода Break влияют на максимальное количество итераций, просмотрев свойство LowestBreakIteration объекта ParallelLoopState, как показано ниже:

Parallel.For(1, 20, (i, pls) =>
{
    Console.WriteLine(string.Format(
        "i={0} LowestBreakIteration={1}", i, pls.LowestBreakIteration));
    if (i >= 15)
    {
        pls.Break();
    }
});
 
/* OUTPUT
              
i=10 LowestBreakIteration=
i=11 LowestBreakIteration=
i=19 LowestBreakIteration=
i=1 LowestBreakIteration=
i=2 LowestBreakIteration=19
i=3 LowestBreakIteration=19
i=6 LowestBreakIteration=19
i=7 LowestBreakIteration=19
i=8 LowestBreakIteration=19
i=9 LowestBreakIteration=19
i=12 LowestBreakIteration=19
i=13 LowestBreakIteration=19
i=14 LowestBreakIteration=19
i=15 LowestBreakIteration=19
i=4 LowestBreakIteration=19
i=5 LowestBreakIteration=15
              
*/
Когда мы просматриваем результаты цикла, мы видим, что LowestBreakIteration начинается с нулевого значения. Когда одно ядро достигло метода Break на итерации 19, значение свойства изменилось. Обратите внимание, что ни одно другое ядро не замечало этого изменения до итерации 2. Позже, когда выполнялась итерация 15, значение свойства было уменьшено до 15 вторым вызовом Break. Еще две итерации были выполнены, чтобы гарантировать, что итерации 4 и 5 были выполнены.

Использование ParallelLoopState.Stop

В некоторых ситуациях вы захотите, чтобы ваш параллельный цикл завершился как можно быстрее, не пытаясь имитировать результаты последовательного цикла. В этих случаях вы можете использовать метод ParallelLoopState.Stop. Как и в случае с методом Break, итерации, которые уже выполняются параллельно, завершатся до окончательной остановки цикла.

В следующем примере показано использование метода Stop. Здесь мы выполняем цикл для каждого из значений от одного до двадцати. Когда мы находим значение, которое делится на шесть без остатка, вызывается метод Stop.

Parallel.For(1, 20, (i, pls) =>
{
    Console.Write(i + " ");
    if (i % 6 == 0)
    {
        Console.WriteLine("Stop on {0}", i);
        pls.Stop();
    }
});
 
/* 
 
1 10 19 2 3 6 Остановка на 6
11 4
 
* /
В результатах, показанных в комментариях к коду, вы можете увидеть, что одно ядро вызвало Stop, когда было найдено число шесть. Итерации 11 и 4 уже были запланированы, поэтому они завершились до завершения цикла. Ключевое отличие здесь в том, что итерация 5 никогда не встречалась, как это было бы, если бы вместо Stop использовался Break. Обратите внимание, что вызов метода Stop по-прежнему возможен для нескольких итераций.
Автор этого материала - я - Пахолков Юрий. Я оказываю услуги по написанию программ на языках Java, C++, C# (а также консультирую по ним) и созданию сайтов. Работаю с сайтами на CMS OpenCart, WordPress, ModX и самописными. Кроме этого, работаю напрямую с JavaScript, PHP, CSS, HTML - то есть могу доработать ваш сайт или помочь с веб-программированием. Пишите сюда.

тегистатьи IT, параллельное программирование, си шарп, циклы




Отправляя сообщение я подтверждаю, что ознакомлен и согласен с политикой конфиденциальности данного сайта.




Перехват очереди печати на C#
Урок 5. Статика C#
Попаданец в Каменный век