Потенциальные ошибки, связанные с параллелизмом данных и задач
Во многих случаях метод Parallel.For и метод Parallel.ForEach могут предоставить значительные улучшения производительности по сравнению с последовательными циклами. Однако параллельное выполнение циклов повышает сложность, что может привести к проблемам, которые в последовательном коде встречаются не так часто или не встречаются вовсе. В этом разделе перечислены некоторые рекомендации по тому, чего следует избегать при написании параллельных циклов.
Пример медленной параллельной обработки
В отдельных случаях параллельный цикл может выполняться медленнее, чем его последовательный эквивалент. Основное эмпирическое правило заключается в том, что параллельные циклы, которые содержат несколько итераций и быстрых пользовательских делегатов, скорее всего сильно не увеличат скорость. Однако поскольку на производительность оказывает влияние множество факторов, рекомендуется всегда оценивать фактические результаты.
Не следует выполнять запись в общие области памяти
В последовательном коде нередко выполняется чтение из статических переменных или полей класса либо запись в них. Однако при каждом параллельном обращении к таким переменным в нескольких потоках есть большая вероятность состояния гонки. Несмотря на то что для синхронизации доступа к переменной можно использовать блокировки, затраты ресурсов на синхронизацию могут снизить производительность. Поэтому рекомендуется избегать или хотя бы ограничивать доступ к общему состоянию в параллельном цикле, насколько это возможно. Лучший способ сделать это — перегрузка метода Parallel.For и метода Parallel.ForEach, которые используют переменную System.Threading.ThreadLocal<T> для хранения локального состояния потока при выполнении цикла. Дополнительные сведения см. в разделах Практическое руководство. Написание цикла Parallel.For, содержащего локальные переменные потока и Практическое руководство. Написание цикла Parallel.ForEach, содержащего локальные переменные потока.
Избегайте излишней параллелизации
Использование параллельных циклов приводит к чрезмерным затратам ресурсов на разделение исходной коллекции и синхронизацию рабочих потоков. Преимущества параллелизации значительно ограничиваются количеством процессоров в компьютере. При выполнении нескольких потоков, ограниченных по скорости вычислений, на одном процессоре скорость не увеличивается. Таким образом, необходимо избегать использования излишней параллелизации цикла.
Наиболее общим сценарием, при котором может возникнуть излишний параллелизм, являются вложенные циклы. Во многих случаях лучше выполнить параллелизацию только внешнего цикла, за исключением ситуаций когда применяются одно или несколько следующих условий.
Известно, что внутренний цикл выполняется в течение длительного времени.
Для каждого заказа требуются большие затраты компьютерных ресурсов. (Для операции, представленной в примере, не требуется больших затрат ресурсов.)
Известно, что целевая система имеет достаточно процессоров для обработки потоков, которые возникнут в результате параллелизации запроса в cust.Orders.
Во всех случаях наилучшим способом определения оптимальной формы запроса является тестирование и измерение.
Избегайте вызовы к потокоопасным методам
Запись в потокоопасные методы экземпляра из параллельного цикла может привести к повреждению данных, которое может остаться незамеченным в программе. Это также может привести к возникновению исключений. В следующем примере несколько потоков предпримут попытку одновременного вызова метода FileStream.WriteByte, что не поддерживается классом.
Dim fs As FileStream = File.OpenWrite(filepath)
Dim bytes() As Byte
ReDim bytes(1000000)
' ...init byte array
Parallel.For(0, bytes.Length, Sub(n) fs.WriteByte(bytes(n)))
FileStream fs = File.OpenWrite(path);
byte[] bytes = new Byte[10000000];
// ...
Parallel.For(0, bytes.Length, (i) => fs.WriteByte(bytes[i]));
Ограничение вызовов потокобезопасных методов
Большинство статических методов в платформе .NET Framework потокобезопасны и могут вызываться из нескольких потоков одновременно. Однако даже в таких случаях действующая синхронизация может привести к значительному замедлению в запросе.
Примечание |
---|
Это можно проверить самостоятельно, вставив несколько вызовов метода WriteLine в запросы.Хотя этот метод используется в примерах документации в демонстрационных целях, не используйте его в параллельных циклах без крайней необходимости. |
Проблемы, связанные со сходством потоков
Некоторые технологии, например, COM-взаимодействие для компонентов однопотокового подразделения (STA), Windows Forms и Windows Presentation Foundation (WPF), накладывают ограничения на сходство потоков, согласно которым код должен выполняться в определенном потоке. Например, в Windows Forms и WPF элемент управления доступен только в потоке, в котором он был создан. Например, это означает, что нельзя обновить элемент управления "Список" из параллельного цикла, если не настроить планировщик потоков для планирования работы только в потоке пользовательского интерфейса. Дополнительные сведения см. в разделе Практическое руководство. Планирование работы в указанном контексте синхронизации.
Будьте внимательны при ожидании в делегатах, которые вызываются с помощью Parallel.Invoke
В определенных условиях библиотека параллельных задач будет встраивать задачу, это означает, что она выполняется в задаче в потоке, который выполняется в данный момент. (Дополнительные сведения см. в разделе Планировщики заданий.) В определенных случаях оптимизация производительности может привести к блокировке. Например, две задачи могут выполнять одинаковый код делегата, который сигнализирует при наступлении события, а затем ожидает сигнала от другой задачи. Если вторая задача встроена в тот же поток, что и первая задача, и первая задача переходит в состояние ожидания, вторая задача никогда не сможет сигнализировать о своем событии. Для предупреждения таких ситуаций можно указать время ожидания в операции ожидания, либо использовать явные конструкторы потоков, чтобы убедиться, что одна задача не может блокировать другую.
Не следует считать, что итерации в циклах ForEach, For и ForAll всегда выполняются параллельно
Важно помнить, что отдельные итерации в цикле методаFor, ForEach или ForAll<TSource> могут выполняться параллельно, однако это не обязательно. Поэтому не следует создавать код, правильность выполнения которого возможна только при параллельном выполнении итераций или при выполнении итераций в определенной последовательности. Например, велика вероятность взаимоблокировки следующего кода.
Dim mres = 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)
mres.Set()
Else
Console.WriteLine("Waiting on {0} with value of {1}",
Thread.CurrentThread.ManagedThreadId, j)
mres.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
В этом примере одна итерация задает событие, а все остальные итерации ожидают его. Ни одна из ожидающих итераций не может завершиться раньше, чем завершится итерация, задавшая событие. Однако возможно, что ожидающие итерации блокируют все потоки, используемые для выполнения параллельного цикла, до того как будет выполнена итерация, задавшая событие. Это приводит к взаимоблокировке — задающая событие итерация никогда не будет выполнена, а ожидающие итерации никогда не активизируются.
В частности, выполнение одной итерации в параллельном цикле никогда не должно зависеть от выполнения другой итерации цикла. Если параллельный цикл решит запланировать итерации последовательно, но в обратном порядке — возникнет взаимоблокировка.
Избегайте выполнения параллельных циклов в потоке пользовательского интерфейса
Важно сохранять состояние оклика для пользовательского интерфейса приложения. Если операция содержит достаточный объем работы, который может стать основанием для параллелизма, то она не должна выполняться в потоке пользовательского интерфейса. Вместо этого его следует разгрузить, чтобы операция выполнялась в фоновом потоке. Например, если необходимо использовать параллельный цикл для вычисления некоторых данных, которые затем должны быть показаны в элементе управления пользовательского интерфейса, необходимо рассмотреть возможность выполнения цикла в экземпляре задачи, а не непосредственно в обработчике событий пользовательского интерфейса. Только после того, как завершится основная вычислительная задача, следует маршалировать обновление пользовательского интерфейса назад в поток пользовательского интерфейса.
Если принято решение выполнять параллельный цикл в потоке пользовательского интерфейса, будьте внимательны и предупреждайте обновление элементов управления пользовательского интерфейса из цикла. Попытка обновления элементов управления пользовательского интерфейса из параллельного цикла, который выполняется в потоке пользовательского интерфейса, может привести к нарушению состояния, исключениям, задержке обновлений, в зависимости от того, как вызывается обновление пользовательского интерфейса. В следующем примере параллельный цикл блокирует поток пользовательского интерфейса, в котором выполняется, до завершения всех итераций. Однако если итерация цикла выполняется в фоновом потоке (например, как может это делать метод For), вызов Invoke приводит отправке сообщения в поток пользовательского интерфейса и блокировке для ожидания обработки этого сообщения. Так как поток пользовательского интерфейса заблокирован, выполняя метод For, сообщение никогда не будет обработано и поток пользовательского интерфейса будет находиться в состоянии взаимоблокировки.
Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
Dim iterations As Integer = 20
Parallel.For(0, iterations, Sub(x)
Button1.Invoke(Sub()
DisplayProgress(x)
End Sub)
End Sub)
End Sub
private void button1_Click(object sender, EventArgs e)
{
Parallel.For(0, N, i =>
{
// do work for i
button1.Invoke((Action)delegate { DisplayProgress(i); });
});
}
Следующий пример показывает, как предупредить взаимоблокировку с помощью выполнения цикла внутри экземпляра задачи. Поток пользовательского интерфейса не блокируется циклом и сообщение может быть обработано.
Private Sub Button2_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
Dim iterations As Integer = 20
Task.Factory.StartNew(Sub() Parallel.For(0, iterations, Sub(x)
Button1.Invoke(Sub()
DisplayProgress(x)
End Sub)
End Sub))
End Sub
private void button1_Click(object sender, EventArgs e)
{
Task.Factory.StartNew(() =>
Parallel.For(0, N, i =>
{
// do work for i
button1.Invoke((Action)delegate { DisplayProgress(i); });
})
);
}
См. также
Основные понятия
Параллельное программирование в .NET Framework
Потенциальные ошибки, связанные с PLINQ