Поделиться через


Потенциальные ошибки, связанные с PLINQ

Во многих случаях PLINQ может значительно повысить производительность по сравнению с последовательным выполнением запросов LINQ to Objects. Но сам процесс параллелизации запроса может оказаться сложным и вызвать дополнительные проблемы, которые в последовательном коде не типичны или совсем не встречаются. В этом разделе перечислены некоторые рекомендации по составлению запросов PLINQ.

Не считайте, что параллельные процессы всегда быстрее

Иногда параллелизация приводит к тому, что запрос PLINQ выполняется медленнее, чем его эквивалент в LINQ to Objects. Главное правило заключается в том, что запросы с небольшим числом исходных элементов и быстрыми пользовательскими делегатами обычно не дают большого ускорения. Но на производительность влияет множество разных факторов, поэтому мы рекомендуем всегда оценивать фактические результаты при принятии решения об использовании PLINQ. Дополнительные сведения см. в разделе Общее представление об ускорении выполнения в PLINQ.

Избегайте размещения в общей памяти

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

Избегайте излишней параллелизации

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

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

var q = from cust in customers.AsParallel()
        from order in cust.Orders.AsParallel()
        where order.OrderDate > date
        select new { cust, order };
Dim q = From cust In customers.AsParallel()
        From order In cust.Orders.AsParallel()
        Where order.OrderDate > aDate
        Select New With {cust, order}

В этом примере выгоднее параллелизовать только внешний источник данных (клиенты), если не выполняется хотя бы одно из следующих условий:

  • Внутренний источник данных (пользовательские заказы) содержит заведомо огромное число элементов.

  • С каждым заказом вы выполняете дорогостоящие вычисления. (Операция, показанная в примере, не является дорогостоящей.)

  • Целевая система имеет достаточно процессоров для обработки того количества потоков, которое будет создано при параллелизации запроса в cust.Orders.

В любом случае лучший способ определения оптимальной формы запроса — это проверка и измерение. Дополнительные сведения см. в статье Практическое руководство. Измерение производительности запросов PLINQ.

Избегайте вызова методов, небезопасных для потоков

Запись из запроса PLINQ в методы экземпляров, не являющиеся потокобезопасными, может привести к повреждению данных. Такое изменение даже может остаться незамеченным в программе. Кроме того, она может вызывать исключения. В следующем примере несколько потоков одновременно пытаются вызвать метод FileStream.Write, но этот класс не поддерживает такое поведение.

Dim fs As FileStream = File.OpenWrite(…)
a.AsParallel().Where(...).OrderBy(...).Select(...).ForAll(Sub(x) fs.Write(x))
FileStream fs = File.OpenWrite(...);
a.AsParallel().Where(...).OrderBy(...).Select(...).ForAll(x => fs.Write(x));

Ограничение вызовов потокобезопасных методов

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

Примечание.

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

Избегайте ненужных операций упорядочения

Если PLINQ выполняет запрос параллельно, исходная последовательность разделяется на секции, которые могут обрабатываться одновременно в нескольких потоках. По умолчанию порядок обработки этих секций и порядок получения результатов не является прогнозируемым (за исключением отдельных операторов, например OrderBy). Вы можете сообщить PLINQ, что нужно сохранить порядок всех исходных последовательностей, но это снизит производительность. Везде, где это возможно, мы рекомендуем структурировать запросы так, чтобы они не полагались на сохранение порядка. Дополнительные сведения см. в разделе Сохранение порядка в PLINQ.

Выбирайте ForAll вместо ForEach везде, где это возможно

Несмотря на то, что PLINQ выполняет запрос в нескольких потоках, результаты придется собрать в один поток, если используется цикл foreach (For Each в Visual Basic), чтобы перечислитель обращался к ним последовательно. В некоторых случаях это неизбежно. Но если возможно, всегда используйте метод ForAll, чтобы каждый поток мог выводить свои результаты независимо от других. Например, выполняйте запись в потокобезопасную коллекцию типа System.Collections.Concurrent.ConcurrentBag<T>.

Такая же проблема распространяется на Parallel.ForEach. Другими словами, вариант source.AsParallel().Where().ForAll(...) должен быть более предпочтительным, чем Parallel.ForEach(source.AsParallel().Where(), ...).

Помните о проблемах сходства потоков

Некоторые технологии, например COM-взаимодействие для компонентов однопотокового подразделения (STA), Windows Forms и Windows Presentation Foundation (WPF), накладывают ограничения на сходство потоков, требующие, чтобы код выполнялся в определенном потоке. Например, и в Windows Forms, и в WPF элемент управления может быть доступен только в том потоке, в котором он был создан. Если при выполнении запроса PLINQ попытаться получить доступ к общему состоянию элемента управления Windows Forms, в отладчике произойдет исключение. (Этот параметр можно отключить.) Однако если запрос используется в потоке пользовательского интерфейса, вы можете получить доступ к элементу управления из foreach цикла, который перечисляет результаты запроса, так как этот код выполняется только на одном потоке.

Не считайте, что итерации операторов ForEach, For и ForAll всегда выполняются параллельно

Важно помнить, что отдельные итерации цикла Parallel.For, Parallel.ForEach или ForAll иногда могут выполняться параллельно, но это не гарантируется. В связи с этим старайтесь не писать код, который будет зависеть от правильности параллельного выполнения итераций или от выполнения итераций в определенном порядке.

Например, этот код может вызвать взаимоблокировку:

Dim mre = New ManualResetEventSlim()
Enumerable.Range(0, Environment.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, Environment.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

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

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

См. также