Потенциальные ошибки, связанные с параллелизмом данных и задачPotential Pitfalls in Data and Task Parallelism

Во многих случаях Parallel.For и Parallel.ForEach могут значительно повысить производительность по сравнению с обычным выполнением последовательных циклов.In many cases, Parallel.For and Parallel.ForEach can provide significant performance improvements over ordinary sequential loops. В то же время сложность процесса параллелизации может вызывать проблемы, которые в последовательном коде не встречаются или для него не типичны.However, the work of parallelizing the loop introduces complexity that can lead to problems that, in sequential code, are not as common or are not encountered at all. В этом разделе перечислены некоторые рекомендации по написанию параллельных циклов.This topic lists some practices to avoid when you write parallel loops.

Не считайте, что параллельные процессы всегда быстрее.Do Not Assume That Parallel Is Always Faster

В некоторых случаях параллельный цикл может выполняться медленнее, чем аналогичный последовательный.In certain cases a parallel loop might run slower than its sequential equivalent. Первое правило состоит в том, что параллельные циклы с небольшим числом итераций и быстрыми пользовательскими делегатами, скорее всего, большого ускорения не дадут.The basic rule of thumb is that parallel loops that have few iterations and fast user delegates are unlikely to speedup much. Но в связи с тем, что на производительность влияет множество факторов, рекомендуем всегда оценивать фактические результаты.However, because many factors are involved in performance, we recommend that you always measure actual results.

Избегайте размещения в общей памяти.Avoid Writing to Shared Memory Locations

В последовательном коде для чтения и записи часто используются статические переменные и поля классов.In sequential code, it is not uncommon to read from or write to static variables or class fields. Но всякий раз, когда к таким переменным обращаются сразу несколько потоков, может возникать состояние гонки.However, whenever multiple threads are accessing such variables concurrently, there is a big potential for race conditions. Несмотря на то что для синхронизации доступа к переменной можно использовать блокировки, связанные с нею затраты ресурсов могут снизить производительность.Even though you can use locks to synchronize access to the variable, the cost of synchronization can hurt performance. В связи с этим рекомендуем не использовать или хотя бы максимально ограничить обращение к общему состоянию в параллельном цикле.Therefore, we recommend that you avoid, or at least limit, access to shared state in a parallel loop as much as possible. Для этого лучше всего использовать перегрузки Parallel.For и Parallel.ForEach, которые используют переменную System.Threading.ThreadLocal<T> для хранения локального состояния потока во время выполнения цикла.The best way to do this is to use the overloads of Parallel.For and Parallel.ForEach that use a System.Threading.ThreadLocal<T> variable to store thread-local state during loop execution. Дополнительные сведения см. в разделе Практическое руководство. Написание цикла Parallel.For с локальными переменными потока и Практическое руководство. Написание цикла Parallel.ForEach c локальными переменными раздела.For more information, see How to: Write a Parallel.For Loop with Thread-Local Variables and How to: Write a Parallel.ForEach Loop with Partition-Local Variables.

Избегайте излишней параллелизации.Avoid Over-Parallelization

Использование параллельных циклов связано с чрезмерными затратами ресурсов на секционирование исходной коллекции и синхронизацию рабочих потоков.By using parallel loops, you incur the overhead costs of partitioning the source collection and synchronizing the worker threads. Преимущества параллелизации также ограничивает число процессоров на компьютере.The benefits of parallelization are further limited by the number of processors on the computer. Выполнение сразу нескольких потоков с большим количеством вычислений на одном и том же процессоре не повысит производительность.There is no speedup to be gained by running multiple compute-bound threads on just one processor. В связи с этим излишней параллелизации цикла следует избегать.Therefore, you must be careful not to over-parallelize a loop.

Чаще всего излишняя параллелизация возникает во вложенных циклах.The most common scenario in which over-parallelization can occur is in nested loops. Если не выполняется хотя бы одно из следующих условий, в большинстве случаев выгоднее параллелизовать только внешний цикл:In most cases, it is best to parallelize only the outer loop unless one or more of the following conditions apply:

  • Внутренний цикл очень длинный.The inner loop is known to be very long.

  • С каждым заказом вы выполняете дорогостоящие вычисления.You are performing an expensive computation on each order. (Операция, показанная в примере, не является дорогостоящей.)(The operation shown in the example is not expensive.)

  • Целевая система имеет достаточно процессоров для обработки того количества потоков, которое будет создано при параллелизации запроса в cust.Orders.The target system is known to have enough processors to handle the number of threads that will be produced by parallelizing the query on cust.Orders.

В любом случае лучший способ определения оптимальной формы запроса — это проверка и измерение.In all cases, the best way to determine the optimum query shape is to test and measure.

Избегайте вызова методов, небезопасных для потоков.Avoid Calls to Non-Thread-Safe Methods

Запись в методы экземпляров, не безопасные для потоков, из параллельного цикла способна привести к повреждению данных, которое может остаться или не остаться незамеченным в программе.Writing to non-thread-safe instance methods from a parallel loop can lead to data corruption which may or may not go undetected in your program. Кроме того, она может вызывать исключения.It can also lead to exceptions. В следующем примере несколько потоков одновременно пытаются вызвать метод FileStream.WriteByte, но этот класс не поддерживает такое поведение.In the following example, multiple threads would be attempting to call the FileStream.WriteByte method simultaneously, which is not supported by the class.

FileStream fs = File.OpenWrite(path);
byte[] bytes = new Byte[10000000];
// ...
Parallel.For(0, bytes.Length, (i) => fs.WriteByte(bytes[i]));
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)))

Ограничение вызовов потокобезопасных методовLimit Calls to Thread-Safe Methods

Большинство статических методов на платформе .NET Framework безопасны для потоков и могут вызываться из нескольких потоков одновременно.Most static methods in the .NET Framework are thread-safe and can be called from multiple threads concurrently. Но даже в этих случаях соответствующая синхронизация может значительно замедлить запрос.However, even in these cases, the synchronization involved can lead to significant slowdown in the query.

Примечание

Вы можете проверить это самостоятельно, добавив в запросы несколько вызовов WriteLine.You can test for this yourself by inserting some calls to WriteLine in your queries. Несмотря на то что в примерах документации этот метод часто приводится для демонстрации, не используйте его в параллельных циклах без необходимости.Although this method is used in the documentation examples for demonstration purposes, do not use it in parallel loops unless necessary.

Помните о проблемах сходства потоков.Be Aware of Thread Affinity Issues

Некоторые технологии, например COM-взаимодействие для компонентов однопотокового подразделения (STA), Windows Forms и Windows Presentation Foundation (WPF), накладывают ограничения на сходство потоков, требующие, чтобы код выполнялся в определенном потоке.Some technologies, for example, COM interoperability for Single-Threaded Apartment (STA) components, Windows Forms, and Windows Presentation Foundation (WPF), impose thread affinity restrictions that require code to run on a specific thread. Например, и в Windows Forms, и в WPF элемент управления может быть доступен только в том потоке, в котором он был создан.For example, in both Windows Forms and WPF, a control can only be accessed on the thread on which it was created. В этом случае вы, например, не сможете обновить элемент управления "список" из параллельного цикла, не настроив планировщик потоков на выполнение задач только в потоке пользовательского интерфейса.This means, for example, that you cannot update a list control from a parallel loop unless you configure the thread scheduler to schedule work only on the UI thread. Дополнительные сведения см. в статье Указание контекста синхронизации.For more information, see Specifying a synchronization context.

Будьте внимательны при ожидании в делегатах, вызываемых методом Parallel.Invoke.Use Caution When Waiting in Delegates That Are Called by Parallel.Invoke

В некоторых случаях библиотека параллельных задач встраивает задачу — это означает, что данная задача выполняется в текущем потоке.In certain circumstances, the Task Parallel Library will inline a task, which means it runs on the task on the currently executing thread. (Дополнительные сведения см. в разделе Планировщики задач.) В некоторых случаях подобная оптимизация производительности может привести к взаимоблокировке.(For more information, see Task Schedulers.) This performance optimization can lead to deadlock in certain cases. Например, две задачи могут выполнять один и тот же код делегата, который подает сигнал, если возникает событие, а затем ожидает сигнала от другой задачи.For example, two tasks might run the same delegate code, which signals when an event occurs, and then waits for the other task to signal. Если вторая задача встроена в тот же поток, что и первая, а первая переходит в состояние ожидания, вторая задача не сможет подать сигнал о своем событии никогда.If the second task is inlined on the same thread as the first, and the first goes into a Wait state, the second task will never be able to signal its event. Чтобы этого избежать, можно указать время ожидания для операции ожидания или использовать явные конструкторы потоков, позволяющие убедиться, что одна задача не будет блокировать другую.To avoid such an occurrence, you can specify a timeout on the Wait operation, or use explicit thread constructors to help ensure that one task cannot block the other.

Не считайте, что итерации операторов ForEach, For и ForAll всегда выполняются параллельно.Do Not Assume that Iterations of ForEach, For and ForAll Always Execute in Parallel

Важно помнить, что отдельные итерации цикла For, ForEach или ForAll иногда могут выполняться параллельно, но это не гарантируется.It is important to keep in mind that individual iterations in a For, ForEach or ForAll loop may but do not have to execute in parallel. В связи с этим старайтесь не писать код, который будет зависеть от правильности параллельного выполнения итераций или от выполнения итераций в определенном порядке.Therefore, you should avoid writing any code that depends for correctness on parallel execution of iterations or on the execution of iterations in any particular order. Например, этот код может вызвать взаимоблокировку:For example, this code is likely to deadlock:

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
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

В этом примере одна итерация задает событие, а все остальные его ожидают.In this example, one iteration sets an event, and all other iterations wait on the event. Ни одна из ожидающих итераций не может быть завершена, пока не завершится итерация, задающая событие.None of the waiting iterations can complete until the event-setting iteration has completed. При этом ожидающие итерации способны заблокировать все потоки, которые используются для выполнения параллельного цикла, прежде чем будет выполнена итерация, задающая событие.However, it is possible that the waiting iterations block all threads that are used to execute the parallel loop, before the event-setting iteration has had a chance to execute. Это приведет к взаимоблокировке — итерация, задающая событие, никогда не будет выполнена, а ожидающие итерации никогда не активизируются.This results in a deadlock – the event-setting iteration will never execute, and the waiting iterations will never wake up.

Таким образом, для выполнения работы необходимо, чтобы ни одна итерация параллельного цикла не ожидала другой итерации цикла.In particular, one iteration of a parallel loop should never wait on another iteration of the loop to make progress. Если параллельный цикл решит запланировать итерации последовательно, но в обратном порядке, может возникнуть взаимоблокировка.If the parallel loop decides to schedule the iterations sequentially but in the opposite order, a deadlock will occur.

Избегайте выполнения параллельных циклов в потоке пользовательского интерфейса.Avoid Executing Parallel Loops on the UI Thread

Пользовательский интерфейс всегда должен реагировать на действия пользователя.It is important to keep your application's user interface (UI) responsive. Если операция содержит достаточный объем работы для параллелизации, то, скорее всего, выполнять ее в потоке пользовательского интерфейса не следует.If an operation contains enough work to warrant parallelization, then it likely should not be run that operation on the UI thread. Вместо этого такую операцию следует разгрузить, обеспечив ее выполнение в фоновом потоке.Instead, it should offload that operation to be run on a background thread. Например, если вы хотите использовать параллельный цикл для вычисления некоторых данных, которые будут затем предоставлены в элемент управления пользовательского интерфейса, рекомендуется выполнить цикл в экземпляре задачи, а не в самом обработчике событий пользовательского интерфейса.For example, if you want to use a parallel loop to compute some data that should then be rendered into a UI control, you should consider executing the loop within a task instance rather than directly in a UI event handler. Маршалировать обновление пользовательского интерфейса обратно в поток пользовательского интерфейса следует только после завершения основных вычислений.Only when the core computation has completed should you then marshal the UI update back to the UI thread.

При выполнении параллельных циклов в потоке пользовательского интерфейса следует избегать обновления элементов управления пользовательского интерфейса из цикла.If you do run parallel loops on the UI thread, be careful to avoid updating UI controls from within the loop. Попытка обновить элементы управления пользовательского интерфейса из параллельного цикла, который выполняется в потоке пользовательского интерфейса, может привести к повреждению состояния, исключениям, отложенным обновлениям и даже взаимоблокировкам в зависимости от того, каким образом вызывается обновление пользовательского интерфейса.Attempting to update UI controls from within a parallel loop that is executing on the UI thread can lead to state corruption, exceptions, delayed updates, and even deadlocks, depending on how the UI update is invoked. В следующем примере параллельный цикл блокирует поток пользовательского интерфейса, в котором он выполняется, до завершения всех итераций.In the following example, the parallel loop blocks the UI thread on which it’s executing until all iterations are complete. Если же итерация цикла выполняется в фоновом потоке (как это может делать For), вызов метода Invoke приводит к передаче сообщения в поток пользовательского интерфейса и блокируется в ожидании обработки этого сообщения.However, if an iteration of the loop is running on a background thread (as For may do), the call to Invoke causes a message to be submitted to the UI thread and blocks waiting for that message to be processed. Так как поток пользовательского интерфейса блокируется при выполнении For, сообщение никогда не будет обработано. Такая ситуация с потоком пользовательского интерфейса называется взаимоблокировкой.Since the UI thread is blocked running the For, the message can never be processed, and the UI thread deadlocks.

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 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

В следующем примере показано, как избежать взаимоблокировки, выполнив цикл в экземпляре задачи.The following example shows how to avoid the deadlock, by running the loop inside a task instance. Цикл не блокирует поток пользовательского интерфейса целиком и не препятствует обработке сообщения.The UI thread is not blocked by the loop, and the message can be processed.

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); });
        })
         );
}
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

См. такжеSee also