PATTERNS OF PARALLEL PROGRAMMING / ПАТТЕРНЫ ПАРАЛЛЕЛЬНОГО ПРОГРАММИРОВАНИЯ: Восхитительные параллельные циклы
| Английский оригинал | Перевод на русский | |
|---|---|---|
Сожалеем, но текст оригинала доступен только зарегистрированным пользователям. |
Возможно наиболее известным шаблоном параллелизма является тот, под который подходят "ошеломляюще параллельные" алгоритмы. Программы, удовлетворяющие этому шаблону, способны распараллеливаться, потому что многие отдельные операции могут работать относительно независимо, с малым количеством зависимостей между операциями (или вообще без них), так что они могут быть эффективно вынесены в параллельные вычисления. Досадно, что к таким программам применяется термин "ошеломляющий", поскольку ничего ошеломляющего в них нет вообще. Фактически, чем больше алгоритмов и задач попадают в область "ошеломляющего параллелизма", тем больше следует улучшать положение дел в индустрии ПО. Поэтому, многие авторы начали использовать альтернативные названия этого шаблона "удобный параллелизм", "приятный параллелизм" и "восхитительный параллелизм", для того чтобы проиллюстрировать настоящую природу этих задач. Если Вы обнаружите, что пытаетесь распараллелить задачу, подходящую под этот паттерн, считайте себя счасливчиком и рассчитывайте, что Ваша работа будет гораздо проще, чем могла бы быть, возможно даже "восхитительной". vzhik 25.03.11 в 08:05 |
|
|
Значительная часть работы во многих приложения и алгоритмах делается с помощью управляющих циклов. Циклы, как никак, позволяют приложению исполнять набор инструкций снова и снова, применяя логику к отдельным сущностям, будь то неделимые значения (как в случае с циклом for), или наборы данных (как в случае с циклом foreach). Многие языки содержат встроенные конструкции управления для таких видов циклов (Microsoft Visual C#® и Microsoft Visual Basic® в том числе: первый с инструкциями for и foreach, второй - с For и For Each). Касательно проблемы параллелизма: сущности, обрабатываемые отдельными итерациями цикла могут выполняться одновременно, поэтому нам нужен механизм для выполнения подобного распараллеливания. vzhik 24.03.11 в 14:32 |
||
|
РЕАЛИЗАЦИЯ КОНСТРУКЦИИ ПАРАЛЛЕЛЬНОГО ЦИКЛА vzhik 24.03.11 в 14:29 |
||
|
Поскольку восхитительные параллельные циклы являются превалирующим шаблоном, важно понимать все детали их работы, а также все договоренности, связанные с паттерном. Для понимания этих основ в дальнейшем, мы напишем простой распараллеленый цикл с помощью .NET Framework 3.5, до включения полноценной поддержки параллельности в .NET Framework 4. vzhik 25.03.11 в 12:19 |
||
|
Во первых, нам нужна сигнатура. Для распараллеливания цикла for, мы реализуем метод, принимающий три параметра: нижняя граница, верхняя граница и делегат для тела цикла, в который поступает целочисленный параметр, представляющий индекс итерации (делегат будет вызываться для каждой итерации). Обратите внимание, что у нас есть несколько вариантов поведения этих параметров. В C# и Visual Basic большая часть циклов for пишется примерно следующим образом: vzhik 25.03.11 в 12:24 |
||
|
При беглом чтении Вы можете подумать, что эти циклы идентичны, но это не так: цикл на Visual Basic выполнит на одну итерацию больше своего аналога на C#. Вот почему Visual Basic включает верхнюю границу, в то время как в C# явно исключается верхняя граница с помощью оператора "меньше". Для наших целей последуем примеру реализации на C#, то есть параметр верхней границы для нашего метода параллельного цикла представляет исключающую верхнюю границу. vzhik 25.03.11 в 12:41 |
||
|
Наша реализация этого метода вызовет тело цикла по разу для каждого элемента в диапазоне [нижняя граница, верхняя граница), и выполнит это максимально параллельно. Для этого следует понять, до каких пор вообще можно распараллеливать. vzhik 25.03.11 в 12:52 |
||
|
Здравый смысл в среде параллелизма часто предполагает, что хорошая реализация использует один поток на ядро. Ведь с таким подходом мы полностью задействуем все ядра. Если окажется больше потоков - операционная система вынуждена будет переключать контекст между ними, что выльется в лишние затраты; если меньше - мы не сможем использовать преимущества всех предлагаемых машин, поскольку по меньшей мере одно ядро останется неиспользованным. Это логика не лишена смысла, по крайней мере для определенного класса задач. Но она также основана на идеализированной и теоретической идее компьютера. Вот пример, о который разбивается это утверждение: для выполнения полезных действий потокам, вовлеченным в параллельные вычисления, требуется доступ к данным, что в свою очередь приводит к обращению к кэшу или основной памяти или диску или сети или другим хранилищам, что в терминах времени доступа, может быть затратным; пока выполняются подобные действия CPU может простаивать. Раз так, пока хорошая реализация параллелизма по умолчанию подразумевает один-поток-на-ядро, готовность к иному распределению может оказаться полезной. Однако для начала мы будем придерживаться договоренности "один-поток-на-ядро". vzhik 25.03.11 в 13:17 |
||
|
В .NET Framework получение числа логических процессоров выполняется с помощью класса System.Environment и свойства ProcessorCount в частности. Внутри .NET получает соответствующее значение при помощи функции операционной системы GetSystemInfo из библиотеки kernel32.dll. vzhik 25.03.11 в 13:24 |
||
|
Это значение необязательно соответствует числу физических процессоров и даже числу физических ядер на машине. Скорее, оно учитывает количество доступных аппаратных потоков. Например, на компьютере с двумя сокетами, каждый из которых имеет по четыре ядра, каждое из которых в свою очередь имеет по два аппаратных потока (иногда упоминаемые как hyperthreads) Environment.ProcessorCount вернет 16. vzhik 25.03.11 в 13:28 |
||
|
Начиная с Windows 7 и Windows Server 2008 R2, операционная система Windows поддерживает более 64 логических процессоров, и по умолчанию (по большей части для устаревших приложений), программный доступ к этим ядрам организован через новую концепцию, известную как "группы процессоров". .NET Framework не предоставляетуправляемый доступ к API групп процессоров, поэтому Environment.ProcessorCount ограничено значением 64 (максимальный размер группы процессоров), даже если на машине имеется больше процессоров. Вдобавок, в 32х-битном процессе ProcessorCount ограничен значением 32 для совместимости с 32х-битной маской, представляющую общность процессоров (требование, согласно которому, поток направляется на исполнение только в определенное подмножество процессоров). vzhik 25.03.11 в 13:39 |
||
|
Итак, мы знаем число целевых процессоров, а следовательно и число потоков, теперь можем заняться созданием потоков для каждого ядра. Каждый из потоков занимается обработкой порции входного диапазона, вызывая предоставленное действие для каждой итерации в порции. Подобная обработка требует другой фундаментальной операции параллельного программирования, такой как партиционирование данных. Эта тема будет подробнее обсуждаться дальше; достаточно отметить, что партиционирование является отличительной концепцией параллельных алгоритмов, выделяющейся из множества парадигм параллелизма. В параллельном программировании набор независимых операций может выполняться одновременно. В параллельном программировании операция сначала должна быть разделена на отдельные атомарные действия, которые могут выполняться одновременно с другими; это разделение и распределение и называется партиционированием. В начальных реализациях мы будем использовать простую схему партиционирования: статическое разделение входного диапазона на порции, по порции для каждого потока. vzhik 26.03.11 в 09:43 |
||
|
Наша начальная реализация vzhik 27.03.11 в 08:50 |
||
|
Отметим несколько интересных соображений по поводу этой реализации. Первое: для каждой порции используется новый поток. Этот поток используется чисто для обработки указанной части и впоследствии уничтожается. У такого подхода есть как положительные, так и отрицательные стороны. Очевидным плюсом является то, что ресурсы потока отданы только нашему циклу, и позволяют операционной системе беспристрастно планировать работу потоков. Однако, этот плюс зачастую перевешивается несколькими значительными недостатками. Одним из которых является стоимость потока. По умолчанию, в .NET Framework 4 потоку выделяется мегабайт для стека, вне зависимости от того, используется ли все это пространство для выполняемых задач или нет. Вдобавок, запуск нового потока и его остановка являются относительно затратными, особенно в сравнении с затратами маленького цикла с условно небольшим количеством итераций и примитивной работой внутри каждой из них. При каждом вызове реализации нашего цикла запускаются и уничтожаются новые потоки. vzhik 27.03.11 в 09:23 |
||
|
Есть и другое, более вредное воздействие: чрезмерная подписка. Поскольку мы движемся в многоядерный мир, умеющие работать параллельно компоненты становятся все более и более распространенными, и было бы замечательно, если бы такие компоненты могли работать совместно. Если компоненты используют циклы наподобие приведенных выше, а следовательно запускают по потоку на ядро, мы получим два компонента, соперничающие за аппаратные ресурсы, заставляя операционную систему тратить больше времени на переключение контекстов между компонентами. Переключение контекстов затратное по многим причинам, включая необходимость сохранения деталей выполняемого потока до того, как операционная система выключит поток и заменит его другим. Возможно, важнее то, что такое переключение контекстов может оказать крайне отрицательное влияние на подсистему кеширования процессора. Когда потоку требуются данные, их нужно откуда-то доставить, зачастую из основной памяти. В современных архитектурах стоимость доступа к данным в основной памяти относительно выше, чем исполнение нескольких инструкций над ними. Для уравнивания разработчики процессоров ввели слои кэша, предназначенные для хранения на аппаратном уровне небольшого количества часто используемых данных, доступ к котором значительно эффективнее, чем к основной памяти. Во время выполнения потока, кеш для ядра, на котором он исполняется, стремится заполниться подходящими данными, улучшая его производительность. Когда выключается контекст потока, кеш меняет данные, необходимые новому потоку. Заполнение кэша требует затратных обращений к основной памяти. Поэтому, чем больше переключений контекста между потоками, тем больше будет обращений к памяти, поскольку кеш мечется между нуждами потоков, которые с ним работают. Учитывая эти затраты, чрезмерная подписка может быть причиной серьезных проблем производительности. К счастью, снимки из нового параллельного профилировщика в Visual Studio 2010 могут помочь в обнаружении этих проблем, как показано ниже: vzhik 27.03.11 в 10:29 |
||
|
На этом снимке каждая горизонтальная линия представляет поток, а ось абсцисс представляет время. Зеленым обозначено время исполнения, красным - время блокировки, а желтым время, когда поток мог исполняться, но был вытеснен другим потоком. Чем больше желтого - тем больше чрезмерной подписки, влияющей на производительность. vzhik 27.03.11 в 10:40 |
||
|
Для компенсации затрат, связанных с выделением потока на каждый цикл, мы можем прибегнуть к пулам потоков. Система умеет управлять потоками в этих пулах, направляя в них рабочие элементы для обработки с последующим возвращением потока в пул, а не его уничтожением. Это устраняет упомянутые ранее недостатки. Поскольку потоки не создаются и уничтожаются постоянно, стоимость их жизненного цикла смягчается общей работой всех обрабатываемых единиц. Более того, менеджер пула потоков может управлять максимальным количеством потоков в пуле, задавая ограничения на потребляемую память и чрезмерные подписки. vzhik 27.03.11 в 11:33 |
||
|
Начиная с .NET Framework 1.0 класс System.Threading.ThreadPool предоставлял подобную функциональность, и хотя его реализация менялась с каждой версией (причем для .NET Framework 4 весьма ощутимо), основной принцип остался тем же: .NET Framework предоставляет пул потоков для обслуживания предоставленных ему рабочих элементов. Статический метод QueueUserWorkItem - основной способ добиться этого. Обновим реализацию параллельного цикла for с учетом этой возможности: vzhik 27.03.11 в 11:45 |
||
|
В ней удалена неэффективная работа нашего приложения, связанная с чрезмерным созданием и удалением потоков, а также уменьшена возможности чрезмерной подписки. Однако, неэффективность была не единственной проблемой нашей реализации. Еще одной может оказаться статическое партиционирование, взятое нами на вооружение. Для задач, требующих приблизительного одинаковой работы на каждой итерации, и запущенных на относительно "спокойном" компьютере (имеется ввиду компьютер, на котором выполняется мало работы помимо целевой задачи), статическое партиционирование является успешным и эффективным способом разбиения нашего набора данных. Однако, если нагрузка на каждой итерации различается, будь то вследствие природы решаемой задачи, либо в вследствие того, что некоторые партиции выполняются дольше из-за вытеснения более важными задачами системы, мы можем столкнуться с дисбалансом нагрузки. Симптом дисбаланса нагрузки весьма заметен на следующих снимках из параллельного профилировщика Visual Studio 2010. vzhik 27.03.11 в 12:12 |
||
|
На этой картинке из профилировщика, ось абсцисс представляет время, а ось ординат - число используемых ядер во время работы приложения. Зеленым обозначено использование процессора нашим приложением, желтым - сторонним приложением, красным - системой, серым - время ожидания. Такой результат возник вследствие не случайного распределения работы в каждой партиции; поэтому одни партиции отработали быстрее других. Вспомнили наши прошлые утверждения об использовании количества потоков, меньшего чем число ядер? Сейчас мы вернулись к этой ситуации, в том смысле, что порция цикла выполняется на меньшем количестве ядер, чем доступно. vzhik 27.03.11 в 13:25 |
||
|
В качестве примера рассмотрим параллельный цикл от 1 до 12 (включительно), где каждая итерация выполняется N секунд, где N - порядковый номер итерации (первая итерация потребует 1 секунду вычислений, вторая - 2 секунды и так далее). В целом, этому циклу потребуется (12*13)/2 = 78 секунд последовательного времени обработки. При идеальной реализации цикла на двухядерной системе, мы можем выполнить его за 39 секунд. Добиться этого можно, если одно ядро займется 6й,10й,11й и12й итерациями, а другое ядро - всеми остальными. vzhik 28.03.11 в 07:22 |
||
|
Однако, при статическом партиционировании, которое мы используем, одному ядру выделится диапазон [1;6], а другому - [7;12]. vzhik 28.03.11 в 07:24 |
||
|
А раз так, первое ядро получит полезной работы на 21 секунду, в то время как второму ядру достанется 57 секунд. Поскольку цикл не завершается, пока не обработаются все итерации - время работы нашего цикла определяется максимальным временем работы любой из партиций, а поэтому цикл закончится через 57 секунд вместо ранее упомянутых теоретических 39. Это снижает возможную производительность приблизительно на 50%, и это только вследствие неэффективного разбиения. Теперь Вы понимаете, почему партиционирование занимает такое важное место в параллельном программировании. vzhik 28.03.11 в 09:39 |
||
|
Возможны различные варианты статического разбиения. Например, вместо распределения диапазонов, мы могли бы использовать некое подобие карусели, где каждому потоку присвоен уникальный идентификатор в диапазоне [0; количество потоков), и где каждый поток обрабатывает индексы цикла, для которых остаток от деления на количество потоков совпадает с идентификатором потока. То есть, при входном диапазоне [0;12) и 4х потоках, поток №0 обработает итерации под индексами 0, 3, 6, 9, поток №1 под индексами 1, 4, 7, 10 и так далее. Если мы применим такого рода разбиение к предыдущему примеру, то вместо 21 секунды для первого потока и 57 - для второго, потребуется 36 и 42 секунды соответственно, что означает гораздо меньший разброс относительно оптимального времени работы (39 секунд). vzhik 28.03.11 в 10:21 |
||
|
Чтобы добиться наилучшего статического разбиения, Вы должны достаточно точно предсказывать время выполнения итераций. Это редко возможно, и приводит к потребности в динамическом разбиении, когда система быстро подстраивается под изменение нагрузки. Этого можно достичь сдвигаясь в другую сторону шкалы издержек партиционирования - сторону балансировки нагрузки. vzhik 28.03.11 в 10:51 |
||
|
Вместо передачи в поток заданного набора индексов для обработки, мы организуем борьбу между потоками за итерацию. Мы создадим пул оставшихся для выполнения итераций, в котором изначально содержатся все итерации. Пока не будут обработаны все итерации, каждый поток опрашивает пул итераций, извлекает итерацию, выполняет ее и повторяет все снова. В такой соревновательной манере мы можем достичь приближения к оптимальному балансу нагрузки (настоящего баланса можно добиться только при знании точного времени выполнения каждой итерации). Если поток застрянет на выполнении длительной итерации, то другие потоки компенсируют это обработкой остальных элементов из пула. Конечно, даже при таком способе, Вы все равно можете не добиться идеального распределения (если один поток застрянет на обработке нескольких частей, значительно больших, чем все остальные), но без точной информации, сколько потребуется времени каждому рабочему элементу, мы мало что можем сделать. vzhik 28.03.11 в 13:58 |
||
|
Вот пример реализации балансировки нагрузки крайней степени. Пул итераций задается одним целочисленным значением, представляющим следующую доступную итерацию, а вовлеченные в расчет потоки автоматически "удаляют элементы" увеличивая счетчик: vzhik 28.03.11 в 14:07 |
||
|
К несчастью, это не является панацеей. Мы пришли на другую сторону шкалы, променяв качественную балансировку нагрузки на дополнительные издержки. В наших предыдущих статических реализациях, порции распределялись по потокам, и поэтому могли быть обработаны независимо от других потоков. Не было необходимости в синхронизации с другими потоками для определения дальнейших действий, поскольку каждый поток мог самостоятельно определить, что работа сделана. Когда итерации выполняют много работы, синхронизация между потоками ничтожна. Но для мало нагруженных итераций, затраты на синхронизацию могут оказаться относительно высокими, даже перекрывающими затраты на выполнение самой работы. vzhik 28.03.11 в 14:21 |
||
|
Представьте себе аналогию: покупки с друзьями в продуктовом магазине. Вы входите в магазин со списком покупок, разрываете список на куски таким образом, что каждый друг отвечает за приобретение позиций из его/ее списка. Если время, требуемое для покупки элементов из всех списков, приблизительно одинаково - Вы хорошо распределили работу в команде, и вероятно потратите значительно меньше времени, чем если бы делали все самостоятельно. А теперь представьте, что каждый список плохо сбалансирован, товары из списка для первого друга разбросаны по всему магазину, в то время, как товары из списка другого - сосредоточены в одном проходе. Вы можете подчеркнуть неравенство, выдавая по одному элементу за раз. Каждый раз при покупке еды друг возвращается к Вам в начало магазина и совместно с Вами выбирает, что покупать дальше. Если отдельная покупка занимает много времени, например заказ специального куска мяса в гастрономическом отделе, затраты на хождение от Вас продавцу и обратно могут быть незначительными. Однако, если нужно просто взять банку с полки, затраты на эти похождения могут оказаться высокими, особенно если несколько банок стояли рядом и их можно было принести за один проход, потратив минимум времени. Вы можете относительно долго делить работу между друзьями и решать что же купить дальше, так что возможно быстрее будет самостоятельно собрать все продукты из списка. vzhik 29.03.11 в 11:25 |
||
|
Конечно, не следует кидаться из одной крайности в другую. Как и в большинстве паттернов, здесь также возможны вариации. Например, в примере с продуктовым магазином, Вы можете указать каждому из друзей брать не по одному, а по несколько товаров за раз. Это смягчает издержки за счет размера партии, но в то же время сохраняется некоторая степень динамизма: vzhik 29.03.11 в 11:30 |
||
|
Неважно, на какие компромиссы между балансировкой и затратами Вы идете, это все равно компромиссы. Для конкретных задач Вы можете написать произвольную реализацию параллельного цикла, удовлетворяющего паттерну, который справляется с задачей наилучшим образом. Однако, это потребует дополнительного кодирования. В целом, хорошим решением считается то, которое показывает хорошие результаты для большинства проблем, минимизируя потери за счет достаточной балансировки. .NET Framework содержит такую реализацию в новом классе System.Threading.Tasks.Parallel. vzhik 29.03.11 в 11:37 |
||
|
PARALLEL.FOR vzhik 29.03.11 в 11:53 |
||
|
Замечательно-параллельные задачи представляют один из наиболее распространенных шаблонов параллельного программирования, и нормально, что при включении в основную библиотеку поддержки параллельного программирования, включают ее и для замечательно-параллельных циклов. В .NET Framework 4 этим занимается класс Parallel из пространства имен System.Threading.Tasks в mscorlib.dll. Класс Parallel имеет только три метода, хотя у каждого есть по несколько сигнатур, одна из которых полностью совпадает с сигнатурой реализованного выше метода MyParallelFor: vzhik 29.03.11 в 12:44 |
||
|
Как и наши предыдущие реализации цикл For принимает три параметра: включающую нижнюю границу, исключающую верхнюю границу и делегат для вызова на каждой итерации.В отличие от нашего метода, он также возвращает значение типа ParallelLoopResult, содержащее подробности выполненного цикла, которые мы рассмотрим позже. vzhik 29.03.11 в 13:11 |
||
|
Внутри, метод For организован на манер наших реализации. По умолчанию, он использует класс ThreadPool для выполнения работы с максимально возможным параллелизмом, и вызывает предоставленный делегат однажды за итерацию. Однако, Parallel.For и его перегрузки предоставляют гораздо больше этого: vzhik 29.03.11 в 13:29 |
||
|
Обработка исключений. Если одна итерация в цикле бросит исключение, будет выполнена попытка скорейшей остановки всех потоков, участвующих в цикле (по умолчанию, исполняемые в тот момент итерации прерваны не будут, но управляющий циклом механизм предотвратит запуск новых итераций). Когда обработка полностью остановлена, все необработанные исключения объединяются и генерируется экземпляр AggregateException. Это исключение предоставляет поддержку нескольких внутренних исключений, в то время как большинство исключений в .NET Framework поддерживают только одно вложенное исключение. Дополнительная информация об AggregateException находится по адресу http://msdn.microsoft.com/magazine/ee321571.aspx. vzhik 29.03.11 в 14:11 |
||
|
Предварительный выход из цикла. Работает аналогично ключевому слову break в C# и Exit For в Visual Basic. Также есть возможность понять должна ли текущая итерация отказаться от результатов своей работы, если в других итерациях происходит предварительный выход из цикла. Это главная причина наличия возвращаемого значения ParallelLoopResult в сигнатуре Parallel.For, которое помогает вызывающему коду понять, закончился ли цикл преждевременно и, если да, то почему. vzhik 30.03.11 в 10:32 |
||
|
Широкие диапазоны. Наряду с перегрузками для работы с 32-х битными диапазонами, предоставляются перегрузки, обслуживающие 64-х битные диапазоны. vzhik 30.03.11 в 10:35 |
||
|
Локальное состояние потока. Некоторые перегрузки предоставляют поддержку локального состояния потока. Подробно эта тема будет обсуждаться позже в разделе паттернов аггрегации. vzhik 30.03.11 в 11:30 |
||
|
Варианты настройки. Можно управлять многими аспектами исполнения цикла, включая ограничение числа задействованных потоков. vzhik 30.03.11 в 11:31 |
||
|
Вложенный параллелизм. Если Вы используете Parallel.For внутри другого Parallel.For, они будут согласованно разделять ресурсы потоков. Также нормально вызывать два Parallel.For одновременно, так как они будут совместно использовать потоковые ресурсы из пула, а не занимать все ядра машины. vzhik 30.03.11 в 13:03 |
||
|
Плавающее число потоков. Parallel.For разработан приспосабливаемым к задачам, чья сложность меняется со временем, таким образом, что некоторым порции требуют больше вычислений, чем другие. Раз так, для обработки цикла было бы лучше, чтобы количество потоков, вовлеченных в расчет, менялось со временем, а не задавалось статически, как было во всех наших реализациях. vzhik 30.03.11 в 13:38 |
||
|
Эффективная балансировка нагрузки. Parallel.For балансирует нагрузку весьма изысканным образом, мощнее показанных ранее механизмов. Он учитывает разнообразные возможные задачи и пытается улучшить эффективность с минимальными затратами. Реализация партиционирования построена на механизме разделения на блоки, в котором размер блока увеличивается с течением времени. Это помогает добиться качественной балансировки нагрузки на малом количестве итераций и минимизации потерь - на большом. Вдобавок он пытается обеспечить, чтобы большинство итераций потока сосредотачивались в одной области для обеспечения хорошего расположения в кеше. vzhik 30.03.11 в 13:38 |
||
|
Parallel.For применим к широкому кругу идеально параллельных задач, предоставляя реализацию этого важнейшего шаблона. В качестве демонстрации такого приложения, примеры параллельного программирования для .NET Framework 4 (доступны по адресу http://code.msdn.microsoft.com/ParExtSamples) содержат трассировщик лучей. Ниже приведен снимок экрана: vzhik 01.04.11 в 12:34 |
||
|
Трассировка лучей - фундаментальная параллельная задача. Каждый отдельный пиксел изображения генерируется посылом воображаемого луча света, проверяя цвет луча после отражения и прохождения сквозь объекты сцены с сохранением конечного цвета. Поэтому каждый пиксел сам по себе, что позволяет обрабатывать их одновременно. Вот примерный участок кода: vzhik 01.04.11 в 13:01 |
||
|
Заметьте, что практически нет разницы между последовательным и параллельным кодом, кроме замены ключевого слова for для C#(For для Visual Basic) на вызов метода Parallel.For vzhik 01.04.11 в 12:38 |
||
|
PARALLEL.FOREACH vzhik 03.04.11 в 17:05 |
||
|
Цикл for является узко специализированным. Его цель - перебирать набор данных определенного типа, набор данных из чисел, представляющих диапазон. Более обобщенной концепцией является обход любого набора данных, что в C# поддерживается с помощью ключевого слова foreach, в Visual Basic - For Each. vzhik 03.04.11 в 14:23 |
||
|
Рассмотрим следующий цикл: vzhik 04.04.11 в 04:39 |
||
|
Задействовав класс Enumerable из LINQ, мы можем создать IEnumerable, представляющий тот же диапазон, и обходить этот набор данных с помощью foreach: vzhik 04.04.11 в 05:00 |
