이전에 알아본 InterLocked는 멀티 스레드 환경에서 사용할 수 있는 중요한 요소 중 하나입니다.
그러나 InterLocked가 가진 치명적인 단점은 정수형 변수만 다룰 수 있다는 점입니다.
따라서 이번에는 다른 방식으로 멀티스레드 환경을 만들어보겠습니다.
멀티스레드 환경에서는 단순히 읽어오는 것은 문제가 되지는 않지만, 쓰기 시작하면서 문제가 발생하기 시작합니다.
그리고 이런 식으로 문제가 발생하는 코드 영역을 Critical Section 한국어로 임계영역이라고 합니다.
1) Monitor 클래스를 통한 임계영역 제한
이럴 때 사용가능한 클래스가 바로 Monitor 클래스이고,
Monitor 클래스의 함수 Enter와 Exit입니다.
다음의 사용예제를 보겠습니다.
class Program
{
static int number = 0;
static object Key = new object();
static void Thread_1()
{
for (int i = 0; i < 100000000; i++)
{
Monitor.Enter(Key);
number++;
Monitor.Exit(Key);
}
}
static void Thread_2()
{
for (int i = 0; i < 100000000; i++)
{
Monitor.Enter(Key);
number--;
Monitor.Exit(Key);
}
}
static void Main(string[] args)
{
Task t1 = new Task(Thread_1);
Task t2 = new Task(Thread_2);
t1.Start();
t2.Start();
Task.WaitAll(t1, t2);
Console.WriteLine(number);
}
}
기존의 InterLocked를 사용하던 방식에서 위와 같이 Monitor 클래스를 사용하는 방식으로 변경하였습니다.
실행결과를 확인해보면 다음과 같습니다.
의도한 대로 잘 작동하는 것을 확인할 수 있습니다.
Monitor 클래스의 Enter 함수는 임계영역을 잠그는 작업을 수행합니다.
잠겨진 동안에는 임계영역에는 하나의 스레드만 작업할 수 있습니다. 따라서 임계영역의 원자성이 보장됩니다.
Monitor의 Exit 함수는 임계영역을 여는 작업을 수행합니다. 이 경우 다른 스레드들이 임계영역에 접근이 가능합니다.
그리고 Monitor 클래스가 수행하는 위의 작업을 하나의 단어로 상호배제라고 표현할 수 있습니다.
위와 같은 상호배제는 C# 말고도 C++ 같은 언어에서는
std::Mutex로 사용할 수 있고, C++ Window환경에서는 CriticalSection으로 사용할 수 있습니다.
2) Monitor의 문제점
이제 Monitor 클래스가 갖고 있는 문제점에 대해 알아보겠습니다.
기존의 코드를 다음과 같이 수정하겠습니다.
class Program
{
static int number = 0;
static object Key = new object();
static void Thread_1()
{
for (int i = 0; i < 100000000; i++)
{
Monitor.Enter(Key);
number++;
//Monitor.Exit(Key);
}
}
static void Thread_2()
{
for (int i = 0; i < 100000000; i++)
{
Monitor.Enter(Key);
number--;
Monitor.Exit(Key);
}
}
static void Main(string[] args)
{
Task t1 = new Task(Thread_1);
Task t2 = new Task(Thread_2);
t1.Start();
t2.Start();
Task.WaitAll(t1, t2);
Console.WriteLine(number);
}
}
Enter하였지만 Exit하지 않는 상황을 만들어주었습니다.
이제 실행결과를 확인해보겠습니다.
위와 같이 종료되지 않고 무한히 반복중인 것을 확인할 수 있습니다.
이런 문제가 발생하는 이유는 스레드가 해당 데이터를 계속 점유하고 있기에 발생합니다.
즉, Enter와 Exit은 반드시 짝을 맞춰 이뤄져야 하고
짝을 맞추지 못하면 스레드가 데이터를 계속 차지하고 마는 DeadLock이 발생하게 됩니다.
그러나 설계자가 일일이 임계영역마다 짝을 맞춰주는 것은 매우 힘든일입니다.
이런 상황에서 사용할 수 있는 매우 유용한 기능이 바로 Lock입니다.
3) Lock을 통한 DeadLock 문제 해결
Lock은 내부적으로는 Monitor를 사용합니다.
하지만, 기본적으로 짝을 맞춰 설계되었기 때문에 매우 안전하고 간편하게 사용할 수 있습니다.
기존의 코드를 다시 한번 아래와 같이 수정하였습니다.
class Program
{
static int number = 0;
static object Key = new object();
static void Thread_1()
{
for (int i = 0; i < 100000000; i++)
{
lock (Key)
{
number++;
}
}
}
static void Thread_2()
{
for (int i = 0; i < 100000000; i++)
{
lock (Key)
{
number--;
}
}
}
static void Main(string[] args)
{
Task t1 = new Task(Thread_1);
Task t2 = new Task(Thread_2);
t1.Start();
t2.Start();
Task.WaitAll(t1, t2);
Console.WriteLine(number);
}
}
이제 실행 결과를 확인해보겠습니다.
DeadLock이 발생하지 않고 무사히 상호 배제를 구현하는 것에 성공하였습니다.
'C#' 카테고리의 다른 글
SpinLock (0) | 2024.12.04 |
---|---|
DeadLock과 Lock구현 (0) | 2024.12.02 |
Interlocked (0) | 2024.11.27 |
Memory Barrier (0) | 2024.11.27 |
캐싱 (1) | 2024.11.27 |