멀티 쓰레드
Lock (OS에서는 크리티컬 섹션(CriticalSection), C++에서는 std::mutex 라고 불린다.)
Interlocked 계열 메서드는 성능도 빠르고 좋긴 하지만, 단점이 존재한다. 바로 정수만 사용할 수 있는 것이다. 우리가 나중에 멀티 쓰레드를 이용해 프로그램을 짤 때에는 단순히 number++ 만을 하지는 않을 것이다. 따라서 특정 신호를 주어서 사용자가 정한 블록 안의 내용은 하나의 쓰레드만 실행하도록 제어할 수 있는 도구가 필요할 것이다.
먼저 소스코드 내 가상의 선을 긋는다고 생각하자. 이 영역은 아무도 접근할 수 없으며, 내가 먼저 점유할 경우 다른 쓰레드는 얼씬도 하지 못한다. 라고 생각하자.
class Program
{
static int number = 0;
static object _obj = new object();
static void Thread_1()
{
for (int i = 0; i < 100000; i++)
{
Monitor.Enter(_obj);
number++;
Monitor.Exit(_obj);
}
}
static void Thread_2()
{
for (int i = 0; i < 100000; i++)
{
Monitor.Enter(_obj);
number--;
Monitor.Exit(_obj);
}
}
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);
}
}
이럴 때 사용하는 것이 바로 Monitor 메서드이다. 이를 비행기 화장실로 생각해보자. 우리는 비행기를 타면 화장실을 사용할 수 있는데 대게 화장실은 1인용이다. 따라서 사용하기 위해서는 문을 잠궈야한다. 이 역할을 하는 것이 바로 Monitor.Enter() 메서드이다. 즉 내가 쓰레드 1에서 문을 잠군다면, 쓰레드 2에서는 문을 잠그려고 하기 전에, 이미 문이 잠겨있기 때문에 문이 열리기까지 대기해야 한다.
따라서 문을 잠고 볼일(쓰레드 1의 number++)을 다 보고 문을 열어주는 것과 같다. (Monitor.Exit()) 반대로 쓰레드 2가 먼저 시작한 경우, 쓰레드 1의 Enter는 쓰레드 2의 Exit이 끝나고 난 이후에 실행된다. 이러한 상황을 상호 배제(Mutual Exclusive) 라고 한다. 나만 사용할 거야! 너 배제* 할거야! (❗❓ 정처기 교착상태(=데드락), 상점비환 (=상호배제, 점유 및 대기, 비선점, 환형대기)의 개념이 또 나온다.)
* 배제 = 물리쳐서 제외하는 것.
그러나 Monitor Enter, Exit도 단점이 없는 것은 아니다. 바로 관리하기가 어려워 진다는 것이다. 만약 쓰레드 1 내부에서 Exit 메서드를 정상적으로 실행하지 않고 return; 을 실행해버릴 경우, 프로그램은 무한루프에 빠지는 것이다. 비행기 화장실로 비유하면 화장실에 들어간 사람이 그만 화장실에서 잠이 든 셈이고, 이 사람이 나오기를 기다리고 있던 사람들(쓰레드 2)은 하염없이 화장실을 다 쓰고 나오기만을 기다리고 있는 것이다. 이를 교착상태(=데드락)이라고 한다.
그렇다면 우리가 Enter와 Exit의 짝을 모두 다 맞췄고, number가 10000이 될 경우 쓰레드를 종료한다고 가정해보자.
static void Thread_1()
{
for (int i = 0; i < 100000; i++)
{
Monitor.Enter(_obj);
number++;
if (number == 10000)
{
Monitor.Exit(_obj);
return;
}
Monitor.Exit(_obj);
}
}
벌써부터 코드가 더러워 지는 것을 알 수 있다. 특정 조건, 상황마다 Monitor.Exit(_obj);코드를 일일히 작성해야 하기 때문이다. 심지어 number를 0으로 나누면(divide by zero) 정상적으로 Monitor.Exit(_obj);를 실행하지 않고 문제가 생길 수도 있는 것이다. 따라서 Monitor.Enter/Exit을 사용하려면 try ~ catch문을 사용해야 한다.
static void Thread_1()
{
for (int i = 0; i < 100000; i++)
{
try
{
Monitor.Enter(_obj);
number++;
return;
}
finally
{
Monitor.Exit(_obj);
}
}
}
물론 이렇게 진행한다고 해도 모든 경우를 다 try ~ catch를 사용해야 하기 때문에 번거로운 것은 Monitor.Exit 메서드와 동일한 셈이다. 그래서 이를 해결하기 위해 lock 키워드를 사용한다.
static void Thread_1()
{
lock(_obj)
{
number++;
}
}
static void Thread_2()
{
lock (_obj)
{
number--;
}
}
lock 메서드를 이용해 사용 할경우 try ~ catch와 똑같은 역할을 하고, 내부 구현도 Moniter.Enter(), Exit()으로 이루어져 있다. 그렇지만 이를 보다 편리하게 사용할 수 있도록 관리하는 것이다. lock 메서드를 이용하면 본인이 알아서 잠구고, 알아서 열어준다. 👍
'공부 > 인프런 - Rookiss' 카테고리의 다른 글
Part 4-2-9. 멀티쓰레드 프로그래밍 : Lock 구현 이론 (0) | 2023.09.13 |
---|---|
Part 4-2-8. 멀티쓰레드 프로그래밍 : 데드락 (0) | 2023.09.12 |
Part 4-2-6. 멀티쓰레드 프로그래밍 : interlocked (0) | 2023.09.12 |
Part 4-2-5. 멀티쓰레드 프로그래밍 : 메모리 배리어 (0) | 2023.09.11 |
Part 4-2-4. 멀티쓰레드 프로그래밍 : 캐시 이론 (1) | 2023.09.06 |