데이터 및 작업 병렬 처리에서 발생할 수 있는 문제Potential Pitfalls in Data and Task Parallelism

대부분의 경우 Parallel.ForParallel.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. 이를 수행하는 가장 좋은 방법은 System.Threading.ThreadLocal<T> 변수를 사용하여 루프 실행 중 스레드 로컬 상태를 저장하는 Parallel.ForParallel.ForEach의 오버로드를 사용하는 것입니다.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 루프 작성을 참조하세요.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

STA(단일 스레드 아파트) 구성 요소에 대한 COM 상호 운용성, Windows Forms 및 WPF(Windows Presentation Foundation)와 같은 일부 기술은 특정 스레드에서 실행하는 코드를 필요로 하는 스레드 선호도 제한 사항이 적용됩니다.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. 즉, 예를 들어 스레드 스케줄러를 UI 스레드에서만 작업을 예약하도록 구성하지 않는 한 병렬 루프에서 목록 컨트롤을 업데이트할 수 없습니다.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.

UI 스레드에서 병렬 루프 실행 방지Avoid Executing Parallel Loops on the UI Thread

애플리케이션의 UI(사용자 인터페이스)를 응답 가능한 상태로 유지하는 것은 중요합니다.It is important to keep your application's user interface (UI) responsive. 작업에 병렬화를 보장하는 충분한 작업을 포함하는 경우 해당 작업을 UI 스레드에서 실행되도록 하면 안 됩니다.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. 예를 들어 병렬 루프를 사용하여 UI 컨트롤로 렌더링되어야 하는 일부 데이터를 컴퓨팅하려는 경우 UI 이벤트 처리기에서 직접 실행이 아닌 작업 인스턴스 내 루프 실행을 고려해야 합니다.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. 코어 계산이 완료된 경우에만 UI 업데이트를 UI 스레드로 마샬링해야 합니다.Only when the core computation has completed should you then marshal the UI update back to the UI thread.

UI 스레드에서 병렬 루프를 실행하면 실행하는 경우 루프 내에서 UI 컨트롤을 업데이트하지 않도록 주의해야 합니다.If you do run parallel loops on the UI thread, be careful to avoid updating UI controls from within the loop. UI 스레드에서 실행 중인 병렬 루프 내에서 UI 컨트롤 업데이트를 시도하는 경우 UI 업데이트가 호출되는 방식에 따라 상태 손상, 예외, 지연된 업데이트 및 교착 상태가 발생할 수 있습니다.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. 다음 예제에서 병렬 루프는 모든 반복이 완료될 때까지 실행 중인 UI 스레드를 차단합니다.In the following example, the parallel loop blocks the UI thread on which it’s executing until all iterations are complete. 그러나 루프 반복이 백그라운드 스레드에서 실행 중인 경우(For도 수행할 수 있으므로) Invoke를 호출하면 메시지가 UI 스레드로 제출되고 해당 메시지가 처리될 때까지 기다리는 것이 차단됩니다.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. UI 스레드는 For 실행이 차단되므로 메시지는 처리될 수 없으며 UI 스레드는 교착 상태가 됩니다.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. UI 스레드는 루프에 의해 차단되지 않으며 메시지는 처리될 수 있습니다.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