Ескертпе
Бұл бетке кіру үшін қатынас шегін айқындау қажет. Жүйеге кіруді немесе каталогтарды өзгертуді байқап көруге болады.
Бұл бетке кіру үшін қатынас шегін айқындау қажет. Каталогтарды өзгертуді байқап көруге болады.
Во многих случаях PLINQ может обеспечить значительные улучшения производительности по сравнению с последовательными запросами LINQ to Objects. Однако работа по параллелизации выполнения запроса представляет сложность, которая может привести к проблемам, которые в последовательном коде не являются обычными или не встречаются вообще. В этом разделе перечислены некоторые методики, которые следует избегать при написании запросов PLINQ.
Не предполагайте, что параллель всегда быстрее
Параллелизация иногда приводит к тому, что запрос PLINQ выполняется медленнее, чем эквивалент LINQ to Objects. Основное правило заключается в том, что запросы с небольшим количеством исходных элементов и быстро работающими делегатами пользователей вряд ли будут значительно ускорены. Тем не менее, поскольку многие факторы участвуют в производительности, рекомендуется измерять фактические результаты, прежде чем решить, следует ли использовать PLINQ. Дополнительные сведения см. "Понимание ускорения в PLINQ".
Избегайте записи в области общей памяти
В последовательном коде для чтения и записи часто используются статические переменные и поля классов. Но всякий раз, когда к таким переменным обращаются сразу несколько потоков, может возникнуть состояние гонки. Несмотря на то что для синхронизации доступа к переменной можно использовать блокировки, связанные с нею затраты ресурсов могут снизить производительность. Поэтому рекомендуется как можно больше избегать или по крайней мере ограничить доступ к общему состоянию в запросе PLINQ.
Пример: состояние гонки с общей памятью
В следующем примере демонстрируется состояние гонки, возникающее при записи нескольких потоков в общую переменную. Переменная total обращается и изменяется одновременно несколькими потоками без синхронизации, что приводит к непредсказуемым результатам:
static void DemonstrateRaceCondition()
{
int total = 0;
var numbers = Enumerable.Range(0, 10000);
// UNSAFE: Multiple threads writing to shared variable
numbers.AsParallel().ForAll(n => total += n);
Console.WriteLine($"Total (with race condition): {total}");
// Expected: 49,995,000 but result is unpredictable due to race condition
}
Shared Sub DemonstrateRaceCondition()
Dim total As Integer = 0
Dim numbers = Enumerable.Range(0, 10000)
' UNSAFE: Multiple threads writing to shared variable
numbers.AsParallel().ForAll(Sub(n) total += n)
Console.WriteLine($"Total (with race condition): {total}")
' Expected: 49,995,000 but result is unpredictable due to race condition
End Sub
В этом коде операция total += n не атомарна. Он включает чтение текущего значения total, добавление n, и запись результата обратно в total. При одновременном выполнении этой операции несколько потоков могут считывать одно и то же значение, добавлять его в разных потоках и записывать результаты, которые могут перезаписывать друг друга. Это приводит к потере некоторых дополнений, что приводит к неправильному окончательному результату.
Правильный подход — использовать потокобезопасные операции, которые не требуют общего изменяемого состояния:
static void DemonstrateCorrectApproach()
{
var numbers = Enumerable.Range(0, 10000);
// SAFE: Use thread-safe aggregate operation
int total = numbers.AsParallel().Sum();
Console.WriteLine($"Total (correct): {total}");
// Result is always 49,995,000
}
Shared Sub DemonstrateCorrectApproach()
Dim numbers = Enumerable.Range(0, 10000)
' SAFE: Use thread-safe aggregate operation
Dim total As Integer = numbers.AsParallel().Sum()
Console.WriteLine($"Total (correct): {total}")
' Result is always 49,995,000
End Sub
Метод Sum обрабатывает параллелизацию внутренне в потокобезопасном режиме, обеспечивая правильные результаты без необходимости явной синхронизации. Другие безопасные подходы включают использование Aggregate пользовательских агрегатов или сбор результатов в потокобезопасных коллекциях, таких как ConcurrentBag<T>.
Избегайте чрезмерной параллелизации
При использовании метода 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-взаимодействие для компонентов Single-Threaded Apartment (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
В этом примере одна итерация задает событие, а все остальные его ожидают. Ни одна из ожидающих итераций не может быть завершена, пока не завершится итерация, задающая событие. При этом ожидающие итерации способны заблокировать все потоки, которые используются для выполнения параллельного цикла, прежде чем будет выполнена итерация, задающая событие. Это приведет к взаимоблокировке — итерация, задающая событие, никогда не будет выполнена, а ожидающие итерации никогда не активизируются.
Таким образом, для выполнения работы необходимо, чтобы ни одна итерация параллельного цикла не ожидала другой итерации цикла. Если параллельный цикл решит запланировать итерации последовательно, но в обратном порядке, может возникнуть взаимоблокировка.