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


Потенциальные ловушки с 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), как известно, очень длинный.

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

  • Целевая система, как известно, имеет достаточно процессоров для обработки количества потоков, которые будут производиться путем параллелизации запроса на 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 элемент управления может быть доступен только в том потоке, в котором он был создан. Если вы пытаетесь получить доступ к общему состоянию элемента управления Windows Forms в запросе PLINQ, возникает исключение, если вы работаете в отладчике. (Этот параметр можно отключить.) Однако если запрос используется в потоке пользовательского интерфейса, вы можете получить доступ к элементу управления из 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

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

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

См. также