Потенциальные ошибки, связанные с PLINQ
Во многих случаях PLINQ может обеспечить значительное увеличение производительности по сравнению с последовательными запросами LINQ to Objects. Однако параллельное выполнение запросов повышает сложность, что может привести к проблемам, которые в последовательном коде встречаются не так часто или не встречаются вовсе. В этом разделе перечислены некоторые рекомендации по тому, чего следует избегать при записи запросов PLINQ.
Пример медленной параллельной обработки
Иногда параллелизация приводит к более медленному выполнению запроса PLINQ по сравнению с его эквивалентом LINQ to Objects. Основное эмпирическое правило заключается в том, что скорость запросов, содержащих несколько исходных элементов и быстрых пользовательских делегатов скорее всего сильно не увеличится. Однако вследствие многих факторов, влияющих на производительность, рекомендуется измерять фактические результаты, прежде чем решать, использовать ли PLINQ. Дополнительные сведения см. в разделе Общее представление об ускорении выполнения в PLINQ.
Нежелательная запись в адреса общей памяти
В последовательном коде нередко выполняется чтение из статических переменных или полей класса либо запись в них. Однако при каждом параллельном обращении к таким переменным в нескольких потоках есть большая вероятность состояния гонки. Несмотря на то что для синхронизации доступа к переменной можно использовать блокировки, затраты ресурсов на синхронизацию могут снизить производительность. Поэтому рекомендуется избегать или хотя бы ограничивать доступ к общему состоянию в запросе PLINQ, насколько это возможно.
Нежелательность излишней параллелизации
Использование оператора AsParallel приводит к чрезмерным затратам ресурсов на секционирование исходной коллекции и синхронизацию рабочих потоков. Преимущества параллелизации значительно ограничиваются количеством процессоров в компьютере. При выполнении нескольких потоков, ограниченных по скорости вычислений, на одном процессоре скорость не увеличивается. Таким образом, необходимо избегать использования излишней параллелизации запроса.
Наиболее распространенным сценарием, в котором может возникнуть чрезмерная параллелизация, являются вложенные запросы, как показано в следующем фрагменте.
Dim q = From cust In customers.AsParallel()
From order In cust.Orders.AsParallel()
Where order.OrderDate > aDate
Select New With {cust, order}
var q = from cust in customers.AsParallel()
from order in cust.Orders.AsParallel()
where order.OrderDate > date
select new { cust, order };
В этом случае лучше параллельно обработать только внешний источник данных (клиенты), если не применяется одно или несколько следующих условий.
Известно, что внутренний источник данный (cust.Orders) очень длинный.
Для каждого заказа требуются большие затраты компьютерных ресурсов. (Для операции, представленной в примере, не требуется больших затрат ресурсов.)
Известно, что целевая система имеет достаточно процессоров для обработки потоков, которые возникнут в результате параллелизации запроса в cust.Orders.
Во всех случаях наилучшим способом определения оптимальной формы запроса является тестирование и измерение. Дополнительные сведения см. в разделе Практическое руководство. Измерение производительности запросов PLINQ.
Нежелательные вызовы потокоопасных методов
Запись в потокоопасные методы экземпляра из запроса PLINQ может привести к повреждению данных, которое может или не может остаться незамеченным в программе. Это также может привести к возникновению исключений. В следующем примере несколько потоков предпримут попытку одновременного вызова метода Filestream.Write, что не поддерживается классом.
Dim fs As FileStream = File.OpenWrite(…)
a.Where(...).OrderBy(...).Select(...).ForAll(Sub(x) fs.Write(x))
FileStream fs = File.OpenWrite(...);
a.Where(...).OrderBy(...).Select(...).ForAll(x => fs.Write(x));
Ограничение вызовов потокобезопасных методов
Большинство статических методов в платформе .NET Framework потокобезопасны и могут вызываться из нескольких потоков одновременно. Однако даже в таких случаях действующая синхронизация может привести к значительному замедлению в запросе.
Примечание |
---|
Это можно проверить самостоятельно, вставив несколько вызовов метода WriteLine в запросы.Хотя этот метод используется в примерах документации в демонстрационных целях, не используйте его в запросах PLINQ. |
Необходимость избегать ненужных операций упорядочения
Когда PLINQ выполняет запрос параллельно, он разделяет исходную последовательность на части, которые могут выполняться одновременно в нескольких потоках. По умолчанию порядок обработки этих частей и доставки результатов непредсказуем (за исключением операторов, таких как OrderBy). Можно указать PLINQ сохранить порядок любой исходной последовательности, но это отрицательно влияет на производительность. По возможности рекомендуется структурировать запросы таким образом, чтобы они не зависели от сохранения порядка. Дополнительные сведения см. в разделе Сохранение порядка в PLINQ.
Использование ForAll вместо ForEach по возможности
Несмотря на то что PLINQ выполняет запрос в нескольких потоках, если используются результаты в цикле foreach (For Each в Visual Basic), результаты запроса необходимо возвращать в один поток и обращаться к ним последовательно с помощью перечислителя. В некоторых случаях это неизбежно. Однако по возможности используйте метод ForAll для указания каждому потоку выводить собственные результаты, например, записав в потокобезопасную коллекцию, такую как ConcurrentBag.
То же самое касается метода ForEach(). Другими словами, настоятельно рекомендуется использовать source.AsParallel().Where().ForAll(...), а не
Parallel.ForEach(source.AsParallel().Where(), ...).
Проблемы, связанные со сходством потоков
Некоторые технологии, например, COM-взаимодействие для компонентов однопотокового подразделения (STA), Windows Forms и Windows Presentation Foundation (WPF), накладывают ограничения на сходство потоков, согласно которым код должен выполняться в определенном потоке. Например, в Windows Forms и WPF элемент управления доступен только в потоке, в котором он был создан. При попытке доступа к общему состоянию элемента управления Windows Forms в запросе PLINQ возникает исключение при выполнении в отладчике. (Этот параметр может быть отключен.) Однако если запрос обрабатывается в потоке пользовательского интерфейса, можно получить доступ к элементу управления из цикла foreach, который перечисляет результаты запроса, поскольку этот код выполняется только в одном потоке.
Не следует полагать, что итерации ForEach, For и ForAll всегда выполняются параллельно
Важно помнить, что отдельные итерации в цикле методаFor(), ForEach() или ForAll() могут выполняться параллельно, однако это не обязательно. Поэтому не следует создавать код, правильность выполнения которого возможна только при параллельном выполнении итераций или при выполнении итераций в определенной последовательности.
Например, велика вероятность взаимоблокировки следующего кода.
Dim mre = New ManualResetEventSlim()
Enumerable.Range(0, ProcessorCount * 100).AsParallel().ForAll(Sub(j)
If j = Environment.ProcessorCount Then
Console.WriteLine("Set on {0} with value of {1}", Thread.CurrentThread.ManagedThreadId, j)
mre.Set()
Else
Console.WriteLine("Waiting on {0} with value of {1}", Thread.CurrentThread.ManagedThreadId, j)
mre.Wait()
End If
End Sub) ' deadlocks
ManualResetEventSlim mre = new ManualResetEventSlim();
Enumerable.Range(0, ProcessorCount * 100).AsParallel().ForAll((j) =>
{
if (j == Environment.ProcessorCount)
{
Console.WriteLine("Set on {0} with value of {1}", Thread.CurrentThread.ManagedThreadId, j);
mre.Set();
}
else
{
Console.WriteLine("Waiting on {0} with value of {1}", Thread.CurrentThread.ManagedThreadId, j);
mre.Wait();
}
}); //deadlocks
В этом примере одна итерация задает событие, а все остальные итерации ожидают его. Ни одна из ожидающих итераций не может завершиться раньше, чем завершится итерация, задавшая событие. Однако возможно, что ожидающие итерации блокируют все потоки, используемые для выполнения параллельного цикла, до того как будет выполнена итерация, задавшая событие. Это приводит к взаимоблокировке — задающая событие итерация никогда не будет выполнена, а ожидающие итерации никогда не активизируются.
В частности, выполнение одной итерации в параллельном цикле никогда не должно зависеть от выполнения другой итерации цикла. Если в параллельном цикле итерации будут запланированы к выполнению последовательно, но в обратном порядке, произойдет взаимоблокировка.