동시에 실행되는 여러개의 Thread가 동시에 같은 변수(예에서 Count)를 수정할 경우 문제가 발생할 수 있습니다.
아래 그림을 보세요.
![](http://blogfiles4.naver.net/data33/2008/8/30/243/thread12_teproze.png)
두 Thread가 연산을 했으니까 당연히 Count는 12가 되어야 하지만 결과는 11입니다.
이와 같은 Thread간의 데이터 공유 문제를 해결하기 위해서 .NET Framework은 상황에 맞는 다양한 해결책을 제시하고 있습니다.
Interlocked Class
증가(1증가), 감소(1 감소)와 같은 증감 연산시에 사용합니다. 위의 코드를 Interlocked 클래스를 사용하도록 변경하면 아래와 같습니다.
static void UpdateCount()
{
for(int i=0;i<10000;i++)
{
Interlocked.Increment(ref count);
}
}
Interlocked.Increment(count); 이 코드가 특정 Thread에 의해 실행되는 동안 다른 Thread는 count 변수에 접근할 수 없습니다.
아래는 Interlocked Class가 제공하는 static method 목록입니다.
Add |
두 값을 더한다. |
Decrement |
1을 뺀다. |
Exchange |
두 값을 바꾼다. |
Increment |
1을 더한다. |
Read |
64비트 수를 읽는다. |
Synchronization Locks
Interlocked 클래스를 사용하면 특정 값에 대한 단위실행(atomic operation)을 보장할 수 있습니다만 더 큰 단위인 object에 대한 처리라면 어떻게 될까요. 보통 object는 여러개의 값을 멤버로 가지게 되고 이에 대해 연산이 수행됩니다. 물론 이것도 object내에서 특정 변수 연산에 Interlocked 클래스를 사용하면 되지 않겠냐고 생각할 수도 있겠지만 단순하지 않습니다.
아래 예제를 보세요.
![](http://blogfiles15.naver.net/data25/2008/8/30/14/thread13_teproze.png)
Counter 클래스를 별도로 만들었습니다. 전체 갯수(count) 및 count가 짝수일 경우 1 증가되는 evenCount가 있습니다. UpdateCount 메소드 내에서 이들 값에 대한 조작시 Interlocked를 사용했기때문에 이들 값이 CPU의 레지스터에 올라가 있는 동안 다른 Thread가 접근 못할 것이고 결과는 우리가 원하는 count=10만, evenCount=5만이 될 것입니다....라고 상상해볼 수 있습니다만 살펴보면 문제가 있습니다.
Interlocked.Increment(ref count)는 문제없습니다. 실행결과는 항상 10만으로 정확합니다. 그러나 evenCount가 계산되는 과정을 보세요.
어느정도 진행되어 count 값이 현재 9(홀수) 라고 가정합니다.
1. Thread1 이 Interlocked.Increment(ref count)를 실행하여 count=10 이 된다.
2. 1이 완료되고 Thread1이 Interlocked.Increment(ref evenCount)을 실행하기전에 Thread2 가 Interlocked.Increment(ref count)을 실행되어 count=11 이 된다.
3. Thread1이 Interlocked.Increment(ref evenCount)을 실행하려고 보니가 count값이 11이어서 evenCount에 대한 1증가 연산이 이루어지지 않는다.
이 상황이라면 10일때 evenCount가 1증가 이뤄져야하는 상황이 사라져 버리는 결과가 오게됩니다. 이와 같은 문제를 해결하기 위해 Synchronization lock이라는 기능을 제공합니다.
Synchronization lock 을 이용하면 UpdateCount를 아래와 같이 수정할 수 있습니다.
![](http://blogfiles6.naver.net/data33/2008/8/30/261/thread14_teproze.png)
위처럼 특정 코드 영역을 lock 키워드를 사용해서 묶습니다. 이는 이 코드 영역이 특정 Thread에 의해 실행되는 동안 다른 Thread는 이 코드 영역을 실행할 수 없도록 합니다.
Monitor
Synchronization lock의 이면에는 실제로 Monitor 클래스가 작동을 합니다. 아래는 Monitor 클래스의 주요 static 메쏘드입니다.
Enter |
명시한 object에 lock을 건다. |
Exit |
명시한 object에 걸린 lock을 해제한다. |
TryEnter |
Enter와 같지만 lock을 얻기까지의 최대 시간(timeout)을 지정한다. |
Wait |
lock을 해제하지만 다시 그 lock을 얻을때까지 현재 Thread를 중단한다. |
|
|
Monitor 클래스를 이용해서 앞의 Counter 클래스를 변경하면 아래와 같습니다.
![](http://blogfiles6.naver.net/data25/2008/9/2/85/thread15_teproze.png)
Deadlock
데드락은 두 쓰레드가 동일 자료에 대해서 lock을 동시에 요청함으로서 lock을 얻지 못하고 무한 대기상태에 빠지는 것을 말합니다.
![](http://blogfiles4.naver.net/data35/2008/9/2/147/thread16_teproze.png)
위 코드에서 데드락이 발생하는 절차는 아래와 같습니다.
1. threadA가 먼저 실행되어 MethodA 코드에 따라 data1에 lock을 건다.
2. threadB가 실행되어 MethodB 코드에 따라 data2에 lock을 건다.
3. threadA가 threadB가 잡고 있는 data2에 대한 lock이 풀릴때까지 대기한다.
4. threadB는 threadA가 잡고 있는 data1에 대한 lock이 풀릴때까지 대기한다.
5. 무한대기 상태로 돌입한다.
이러한 데드락에 대한 해결책은 따로 없습니다. 개발자가 신중하게 개발해야 한다는 것 외에는..
조언이 될만한 것이라면 Monitor.TryEnter를 사용해서 타임아웃을 지정하거나 하는 정도일 것입니다. 가능하면 lock 거는 범위를 최소화 하는 것도 좋을 것입니다.
다음에는 Mutex, Semaphore, AutoResetEvent, ManualResetEvent 에 대해서 추가로 알아볼 생각입니다.