Q1. Interlocked계열의 단점을 2개 말해 보세요.
Q2. Monter.Enter(_obj), Monter.Exit(_obj)를 화장실에 비유해 설명해보세요.
Q3. Monter.Enter(_obj), Monter.Exit(_obj)를 사용했을 때 문제점을 말해 보세요. 화장실에 비유해 설명해 보세요.
Q4. 해결방법을 2가지 말해 보세요.
Interlock 계열이 성능 빠르고 우수하긴 한데 치명적인 단점이 있다.
int afterValue = Interlocked.Increment(ref number);
이런 식으로 정수만 사용할 수 있다는 단점이 있다.
나중에 멀티 스레드 코드를 짤 때는
static void Thread_1()
{
for (int i = 0; i < 1000000; i++)
{
int afterValue = Interlocked.Increment(ref number);
}
///code
///
}
여기에 몇 십줄 혹은 몇 백줄의 코드가 들어갈 수 있다. 그거를 스레드마다 Interlocked로 만들 수 없다. 그래서 어느 신호를 줘서. 블록 안에 있는 이 사이의 부분은 하나의 스레드에서만 실행이 되고, 실행하는 동안에는 나머지 애들은 기다려라 그런 명령을 할 수 있는 도구가 필요하게 될거야.
Interlocked는 잠시 삭제를 하고 원래 버전으로 돌아가보도록 하자.
static void Thread_1()
{
for (int i = 0; i < 1000000; i++)
{
number++;
}
}
static void Thread_2()
{
for (int i = 0; i < 1000000; i++)
{
number--;
}
}
눈으론 한줄이지만 3단계에 걸쳐서 이루어지는 코드야.
메모리 읽는 건 문제가 아냐. 문제가 되는건 쓸 때가 문제가 된다.
동시 다발적으로 접근하는 부분을 CriticalSacion이라고 한다. 임계 영역
해결하기 위한 방법중 하나가 Interlocked
선을 그으면 얼씬도 하지 말아라 라는 늬앙스로 해주는 걸 해볼거야.
static object _obj = new object();
일단 object를 만들어 줘야 한다.
사용방법 여러개 있지만 기초적인 걸 해보자.
static void Thread_1()
{
for (int i = 0; i < 1000000; i++)
{
// 상호배제 Mutual Exclusive
Monitor.Enter(_obj); // 문을 잠구는 행위
number++;
Monitor.Exit(_obj); // 잠금을 풀어준다.
}
}
static void Thread_2()
{
for (int i = 0; i < 1000000; i++)
{
Monitor.Enter(_obj);
number--;
Monitor.Exit(_obj);
}
}
이렇게 하고 실행을 해보면,
어떤 역할을 하는 걸까? 화장실 문 잠그는 행위, 볼 일 다 봤으면 Exit 잠금을 풀어준다.
다른 쓰레드에서 먼저 잠그고 들어갔다면 대기를 해야 한다.
상호배제 Muitual Exclusive라고 한다.
이 블럭 안은 싱글 쓰레드라고 생각하고 프로그래밍을 해도 된다.
블럭안의 코드가 길어지면 Interlocked를 다 지정해 주면 너무 많으니까 이런 식으로 범위를 지정해줘서 통 크게 잠궈주게 되는 상황이 온 거.
언어마다 다 이 기능이 마련되어 있다. C++의 경우는
윈도우 운영체제 에서는 CriticalSection이란 이름
C++ 표준으로 가면 std::mutex란 이름으로 구현이 되어 있다.
조금씩 작동하는 방식이 다를지언정 이런식으로 인터페이스를 열고 잠그고 쌍을 맞춰주는 행위로 되어 있다.
근데 이런식으로 Enter와 Exit를 사용하면 문제가 있다.
관리가 어려워짐.
예를 들어 블럭 안에 코드가 엄청 길어진다고 가정을 하자.
이런 저런 작업하다가 실수로 return을 때려버렸다고 가정.
static void Thread_1()
{
for (int i = 0; i < 1000000; i++)
{
Monitor.Enter(_obj); // 잠구는 행위
number++;
return;
Monitor.Exit(_obj); // 풀어준다.
}
}
이런식으로 문은 잠궜는데 풀어주지 않고 나가면
코드를 실행해보면 끝나지 않는 걸 볼 수 있다.
화장실에 비유를 해보면 비행기 화장실에서 문을 잠그고 들어갔는데 잠든 상황이야. 그럼 문은 열리지 않아서 다른 사람들은 들어가려고 대기 하는데 영영 잠궈져 있으니까 무한 대기 상태로 있는 것.
Thread_1은 return을 해서 딴 일을 하러 갔는데 Thread_2는 하염없이 기다리고 있으니까 먹통이 되는 문제가 발생한 거. 이걸 DeadLock이라고 표현한다.
그래서 짝을 꼭 맞춰줘야해.
static void Thread_1()
{
for (int i = 0; i < 1000000; i++)
{
Monitor.Enter(_obj);
number++;
number / 0;
if(number == 10000)
{
Monitor.Exit(_obj);
return;
}
Monitor.Exit(_obj);
}
}
이렇게 return 전에 Exit를 넣어줘서 맞춰줬다고 해도 number를 0으로 나누거나 하면 바로 나가버리는 상황이 발생한다. 그런 경우 Exit가 챙겨지지 않을거야.
일반적인 상황에서 얘를 사용하고 싶다면 try, catch를 이용해서
static void Thread_1()
{
for (int i = 0; i < 1000000; i++)
{
try
{
Monitor.Enter(_obj);
number++;
return;
}
finally
{
Monitor.Exit(_obj);
}
}
}
이게 첫번재 해결책이 될 수 있다. try에서 return을 하면 finally의 Exit는 무조건 한번은 실행이 될거야.
그렇다고 해도 try finally를 일일이 넣어주는 것도 여간 번거로운 일이 아냐.
그래서 Monitor.Enter를 사용하는 경우는 거의 없고, 대부분의 경우 lock이란 키워드를 사용하게 될거야.
lock(_obj)
{
}
lock도 내부 구현은 Monitor.Enter, End로 이루어져 있다. lock으로 편리하게 만들어 줄 수 있다.
static void Thread_1()
{
for (int i = 0; i < 1000000; i++)
{
lock(_obj)
{
number++;
}
}
}
static void Thread_2()
{
for (int i = 0; i < 1000000; i++)
{
lock (_obj)
{
number--;
}
}
}
데드락을 조금이라도 줄일 수 있는 유용한 방법
'Server programming' 카테고리의 다른 글
01_09_멀티쓰레드_Lock 구현 이론 (0) | 2023.03.14 |
---|---|
01_08_멀티쓰레드_DeadLock (0) | 2023.03.14 |
01_06_멀티쓰레드_Interlocked (0) | 2023.03.13 |
01_05_멀티쓰레드_메모리 베리어 (0) | 2023.03.13 |
01_04_멀티쓰레드_캐시이론 (0) | 2023.03.12 |
댓글