멀티스레딩에는 신중한 프로그래밍이 필요합니다. 대부분의 태스크에서는 스레드 풀 스레드에서 실행 요청을 큐에 대기하여 복잡성을 줄일 수 있습니다. 이 항목에서는 여러 스레드의 작업 조정 또는 차단하는 스레드 처리와 같은 더 어려운 상황에 대해 설명합니다.
비고
.NET Framework 4부터 작업 병렬 라이브러리 및 PLINQ는 다중 스레드 프로그래밍의 복잡성과 위험을 줄이는 API를 제공합니다. 자세한 내용은 .NET의 병렬 프로그래밍을 참조하세요.
교착 상태 및 경합 상태
다중 스레딩은 처리량 및 응답성 문제를 해결하지만, 이렇게 하면 교착 상태 및 경합 상태와 같은 새로운 문제가 발생합니다.
교착 상태
교착 상태는 두 스레드가 각각 다른 스레드가 이미 잠근 리소스를 잠그려고 할 때 발생합니다. 두 스레드 모두 더 이상 진행할 수 없습니다.
관리되는 스레딩 클래스의 많은 메서드는 교착 상태를 감지하는 데 도움이 되는 시간 제한을 제공합니다. 예를 들어 다음 코드는 이름이 지정된 lockObject
개체에 대한 잠금을 획득하려고 시도합니다. 30밀리초 내에 잠금을 가져오지 않으면 Monitor.TryEnter는 false
을 반환합니다.
If Monitor.TryEnter(lockObject, 300) Then
Try
' Place code protected by the Monitor here.
Finally
Monitor.Exit(lockObject)
End Try
Else
' Code to execute if the attempt times out.
End If
if (Monitor.TryEnter(lockObject, 300)) {
try {
// Place code protected by the Monitor here.
}
finally {
Monitor.Exit(lockObject);
}
}
else {
// Code to execute if the attempt times out.
}
레이스 조건
경합 상태는 프로그램의 결과가 두 개 이상의 스레드 중 특정 코드 블록에 먼저 도달하는 경우에 발생하는 버그입니다. 프로그램을 여러 번 실행하면 다른 결과가 생성되며 지정된 실행의 결과를 예측할 수 없습니다.
경합 상태의 간단한 예는 필드의 값을 증가시키는 것입니다. 클래스에 (C#) 또는 (Visual Basic)과 같은 코드를 사용하여 클래스의 인스턴스를 만들 때마다 증가되는 프라이빗 objCt++;
필드(Visual Basic에서 objCt += 1
됨)가 있다고 가정합니다. 이 작업을 수행하려면 레지스터로 값을 objCt
로드하고, 값을 증가시키고, 저장해야 합니다 objCt
.
다중 스레드 애플리케이션에서 값을 로드하고 증분한 스레드는 세 단계를 모두 수행하는 다른 스레드에 의해 선점될 수 있습니다. 첫 번째 스레드가 실행을 다시 시작하고 해당 값을 저장하면 중간에 값이 변경되었다는 사실을 고려하지 않고 덮어씁 objCt
니다.
이러한 특정 경합 상태는 Interlocked 클래스의 메서드(예: Interlocked.Increment)를 사용하여 쉽게 방지할 수 있습니다. 여러 스레드 간에 데이터를 동기화하는 다른 기술에 대해 알아보려면 다중 스레딩을 위한 데이터 동기화를 참조하세요.
여러 스레드의 활동을 동기화할 때도 경합 상태가 발생할 수 있습니다. 코드 줄을 작성할 때마다, 그 줄을 실행하기 전에 스레드가 어떤 이유로 선점되어 실행이 중단될 수 있다는 점을 고려해야 합니다. 또한, 줄을 구성하는 각 기계어 명령이 실행되기 전에 다른 스레드가 이를 선점하는 상황도 염두에 두어야 합니다.
정적 멤버 및 정적 생성자
클래스 생성자(static
Visual Basic의 경우 C# Shared Sub New
의 생성자)가 실행을 완료할 때까지 클래스가 초기화되지 않습니다. 초기화되지 않은 형식에 대한 코드 실행을 방지하기 위해 공용 언어 런타임은 클래스 생성자 실행이 완료될 때까지 다른 스레드 static
에서 클래스 멤버(Shared
Visual Basic의 멤버)로의 모든 호출을 차단합니다.
예를 들어 클래스 생성자가 새 스레드를 시작하고 스레드 프로시저가 클래스의 멤버를 static
호출하는 경우 클래스 생성자가 완료될 때까지 새 스레드가 차단됩니다.
이는 생성자를 가질 수 있는 모든 형식에 static
적용됩니다.
프로세서 수
여러 프로세서가 있는지 또는 시스템에서 사용할 수 있는 프로세서가 하나만 있는지 여부는 다중 스레드 아키텍처에 영향을 줄 수 있습니다. 자세한 내용은 프로세서 수를 참조하세요.
런타임에 사용 가능한 프로세서 수를 확인하기 위해서는 Environment.ProcessorCount 속성을 사용하세요.
일반 권장 사항
여러 스레드를 사용하는 경우 다음 지침을 고려합니다.
다른 스레드를 종료하는 데는 사용하지 Thread.Abort 마세요. 다른 스레드를 호출
Abort
하는 것은 해당 스레드가 처리에서 도달한 지점을 모르고 해당 스레드에 예외를 throw하는 것과 비슷합니다.Thread.Suspend 및 Thread.Resume를 사용하여 여러 스레드의 활동을 동기화하지 마세요. 반드시 Mutex, ManualResetEvent, AutoResetEvent, 및 Monitor를 사용하십시오.
기본 프로그램에서 작업자 스레드의 실행을 제어하지 마세요(예: 이벤트 사용). 대신 작업자 스레드가 작업을 사용할 수 있을 때까지 대기하고, 실행하고, 완료되면 프로그램의 다른 부분에 알릴 수 있도록 프로그램을 디자인합니다. 작업자 스레드가 차단되지 않는 경우 스레드 풀 스레드를 사용하는 것이 좋습니다. Monitor.PulseAll 는 작업자 스레드가 차단되는 경우에 유용합니다.
형식을 잠금 개체로 사용하지 마세요. 즉, C#나 Visual Basic에서
lock(typeof(X))
코드를 사용하거나SyncLock(GetType(X))
개체와 함께 Monitor.Enter를 사용하지 마세요. 지정된 형식의 경우 애플리케이션 도메인당 하나의 인스턴스 System.Type 만 있습니다. 잠금을 적용하는 형식이 public이면 다른 사람의 코드도 잠금을 설정할 수 있어 교착 상태가 발생할 수 있습니다. 추가 문제는 안정성 모범 사례를 참조하세요.예를 들어 C# 또는
lock(this)
Visual Basic과 같이SyncLock(Me)
인스턴스를 잠글 때는 주의해야 합니다. 애플리케이션의 다른 코드(형식 외부)가 개체를 잠그면 교착 상태가 발생할 수 있습니다.모니터에 들어간 스레드가 모니터에 있는 동안 예외가 발생하더라도 항상 해당 모니터를 떠나도록 하세요. C# lock 문과 Visual Basic SyncLock 문은 이 동작을 자동으로 제공하여 최종 블록을 사용하여 호출되도록 Monitor.Exit 합니다. Exit가 호출되는지 확인할 수 없는 경우 뮤텍스를 사용하도록 디자인을 변경하는 것이 좋습니다. 현재 소유하고 있는 스레드가 종료되면 뮤텍스가 자동으로 해제됩니다.
다른 리소스가 필요한 작업에 여러 스레드를 사용하고 단일 리소스에 여러 스레드를 할당하지 마세요. 예를 들어 I/O와 관련된 모든 작업은 해당 스레드가 I/O 작업 중에 차단되어 다른 스레드가 실행되도록 허용하므로 자체 스레드를 갖는 이점을 얻을 수 있습니다. 사용자 입력은 전용 스레드의 이점을 제공하는 또 다른 리소스입니다. 단일 프로세서 컴퓨터에서 집약적인 계산을 포함하는 작업은 사용자 입력과 I/O를 포함하는 태스크와 공존하지만 계산 집약적인 여러 작업이 서로 경합합니다.
간단한 상태 변경에 Interlocked 클래스의 메서드를 사용하는 것을 고려하세요, Visual Basic에서는
lock
문을SyncLock
대신 사용할 수 있습니다. 이lock
문장은 좋은 범용 도구이지만 원자성을 유지해야 하는 업데이트에 대해서는 Interlocked 클래스가 더 나은 성능을 제공합니다. 내부적으로 경합이 없는 경우 하나의 잠금 프리픽스를 실행합니다. 코드 검토에서 다음 예제와 같은 코드를 확인합니다. 첫 번째 예제에서는 상태 변수가 증가합니다.SyncLock lockObject myField += 1 End SyncLock
lock(lockObject) { myField++; }
메서드 Increment를 문
lock
대신 사용하여 성능을 향상시킬 수 있습니다.System.Threading.Interlocked.Increment(myField)
System.Threading.Interlocked.Increment(myField);
비고
1보다 큰 원자적 증가를 위해 Add 메서드를 사용하십시오.
두 번째 예제에서는 참조 형식 변수가 null 참조인 경우에만 업데이트됩니다(
Nothing
Visual Basic의 경우).If x Is Nothing Then SyncLock lockObject If x Is Nothing Then x = y End If End SyncLock End If
if (x == null) { lock (lockObject) { x ??= y; } }
다음과 같이 메서드를 대신 사용하여 CompareExchange 성능을 향상시킬 수 있습니다.
System.Threading.Interlocked.CompareExchange(x, y, Nothing)
System.Threading.Interlocked.CompareExchange(ref x, y, null);
비고
CompareExchange<T>(T, T, T) 메서드 오버로드는 참조 형식에 대한 형식 안전 대안을 제공합니다.
클래스 라이브러리에 대한 권장 사항
다중 스레딩을 위해 클래스 라이브러리를 설계할 때 다음 지침을 고려하십시오.
가능하면 동기화가 필요하지 않습니다. 이는 특히 많이 사용되는 코드에 해당합니다. 예를 들어 알고리즘을 제거하는 대신 경합 상태를 허용하도록 조정될 수 있습니다. 불필요한 동기화로 인해 성능이 저하되고 교착 상태와 경합 상태가 발생할 수 있습니다.
기본적으로 고정 데이터(
Shared
Visual Basic의 경우) 스레드를 안전하게 만듭니다.기본적으로 인스턴스 데이터 스레드를 안전하게 만들지 마세요. 스레드로부터 안전한 코드를 만들기 위해 잠금을 추가하면 성능이 저하되고 잠금 경합이 증가하며 교착 상태가 발생할 수 있습니다. 일반적인 애플리케이션 모델에서는 한 번에 하나의 스레드만 사용자 코드를 실행하므로 스레드 안전성 요구가 최소화됩니다. 따라서 .NET 클래스 라이브러리는 기본적으로 스레드로부터 안전하지 않습니다.
정적 상태를 변경하는 정적 메서드를 제공하지 않습니다. 일반적인 서버 시나리오에서 정적 상태는 요청 간에 공유됩니다. 즉, 여러 스레드가 동시에 해당 코드를 실행할 수 있습니다. 이렇게 하면 스레딩 버그가 발생할 수 있습니다. 요청을 통해 공유되지 않는 인스턴스로 데이터를 캡슐화하는 디자인 패턴을 사용하는 것이 좋습니다. 또한 정적 데이터가 동기화되는 경우 상태를 변경하는 정적 메서드 간의 호출로 인해 교착 상태 또는 중복 동기화가 발생하여 성능에 부정적인 영향을 줄 수 있습니다.
참고하십시오
.NET