Synchronization object

2022. 1. 19. 23:55게임서버/멀티쓰레드 이론

320x100

안녕하세요 대학생 개발자입니다.

제가 졸업하기까지 이제 거의 한달정도 남았는데

닉네임을 뭐로 바꿔야하는지 참고민이 많습니다... 그것보다 닉네임은 그렇다 치더라고... 블로그 주소는 어찌합니까...

이런 고민들이 참 많습니다...

 

째뜬 이건 중요하지 않습니다 중요한건 오늘 할 내용이겠죠

자 보면 저번 글에서까지 윈도우즈의 스케쥴링과 컨텍스트 스위칭에 대해서 간단히 알아봤습니다.

저부분에 관한건 계속 언급될겁니다 매우 중요하기 때문이죠...

 

일단 동기화 객체에 얘기하기 전에 동기화 객체가 왜 필요한지에 대한 언급이 먼저 나와야 될 것 같습니다.

int g_Counter;

void WorkerThread()
{
	for(int i=0; i< 1000000 ; i++)
    	g_Counter++;
}

이런 코드가 있다고 하겠습니다.

이런 경우 싱글스레드라면 당연히 g_Counter는 100만이 찍혀있을겁니다.

그 누구도 부정할 수 없을겁니다.

 

하지만 2개의 쓰레드가 동일한 코드를 돌리고있는 멀티스레드 상황이라면 어떤 일이 일어나는가...

결과부터 말씀드리면 항상은 아니겠지만 높은 확률로 200만보다 낮은 값이 나올겁니다.

 

왜 이런일이 일어나는 것일까요...

이유는 컨텍스트 스위칭으로 인해서 동기화 전의 값이 읽어지는 상황 때문에 그렇습니다.

g_Counter++ 이라는 코드는 cpu 입장에서 단 한번에 처리할 수 있는 문장이 아닙니다. 물론 c++에서는 한번에 처리되는 것 처럼 보이지만

저 코드를 x86 asm으로 가져와본다면...

MOV eax dword ptr [g_Counter]
ADD eax 1
MOV dword ptr [g_Counter] eax

와 같은 코드가 나오게 될겁니다.

그리고 저번글에서 언급했듯 인터럽트를 실행하는 부분은 cpu차원에서의 최소단위 operation 사이와 사이에서 발생이 된다고 말씀드렸습니다.

 

즉 MOV와 ADD사이에서 시스템 타이머의 인터럽트가 올 수도있는것이구요...

ADD와 MOV 사이에서 또 시스템 타이머의 인터럽트가 올 수 있는겁니다.

 

그리고 시스템 타이머의 인터럽트가 올때마다 컨텍스트 스위칭이 일어날 가능성이 있는 것이죠...

우선 cpu는 모든 메모리에 직접 접근해서 연산이 불가능합니다.

그리고 메모리 자체도 연산의 기능이 없습니다.

그러므로 메모리에 있는 값을 레지스터로 가져와서 연산을 하고 그 값을 다시 메모리에 쓰게 되는데요...

여기서 문제는 변수를 가져와서 증가 시켰는데 그순간 context switching이 일어나서 그 변수를 저장하지 못했다면...

그러면 그 쓰레드에서는 g_Counter의 값을 읽어왔을땐 증가되지 않은 g_Counter의 값이 오게 되는겁니다.

그러면 그 g_Counter의 값을 다시 증가시켜서 저장해봤자 다시 context switching이 되었을때 다른 쓰레드에서는 아까 멈췄던 부분부터 실행하기 때문에 g_Counter의 값은 아까 오래전에 저장 못했었던 1증가 시켰던 값으로 저장될겁니다.

 

뭐 그순간 동기화 문제가 발생하는 것이겠죠...

두개의 스레드에서 사용하는 공유자원에 대한 동기화가 안이루어 진 케이스가 되겠죠...

 

네... 뭐 이건 동기화가 필요한 아주 심플하고 아주 러프한 하나의 예시에 불과합니다.

실제 코드는 더더욱 복잡할것이고 이거보다 더 말도안되는 상황이 나올수도 있습니다.

어떤 컨테이너를 순회하는동안 그와 동시에 삭제가 일어나는 그런 말도안되는 경우가 생길수도있는거구요...

이미 종료된 세션을 대상으로 

 

이런 경우가 생기기 때문에 공유자원에 대한 접근이 존재하는 부분에서 동기화가 확실히 이루어져야 하겠죠.

 

그럼 동기화를 어떻게 걸것이냐...

뭐 일단 지금 당장 위에서 말씀드렸던 것 처럼 공유자원에 대놓고 동기화를 걸어도 상관 없습니다.

아까와 같이 g_Counter 변수를 사용하는 상황이라면 무조건 그 전에 g_CounterLock이라는 동기화 객체를 두고 락을 걸고 나올때 락을 풀고 나오면 되는거니까요...

하지만 이런게 늘어나면 늘어날 수록 코드가 복잡해지고 데드락에 걸릴 확률이 높아집니다.

 

한 코드의 예시를 보겠습니다.

int g_Counter1;
int g_Counter2;

void lock1();
void unlock1();
void lock2();
void unlock2();
//각 전역변수당 동기화를 위한 함수 (정의는 밑에 어딘가에 있다고 가정)

void Thread0()
{
    lock2();
    g_Counter2++;
    unlock2();
    
    lock1();
    g_Counter1++;
    unlock1();
}
void Thread1()
{
    lock1();
    g_Counter1++;
    unlock1();
    
    lock2();
    g_Counter2++;
    unlock()2;
}

void Thread2()
{
    lock1();
    g_Counter1 = 10;
    
    if(g_Counter1==10)
    {
        lock2();
        g_Counter2++;
        unlock2();
    }
    unlock1();
}

void Thread3()
{
    lock2();
    g_Counter2 = 10;
    
    if(g_Counter2 == 10)
    {
    	lock1();
        g_Counter1++;
        unlock1();
    }
    unlock2();
}

이런 경우 Thread0,Thread1 의 경우에는 전혀 문제가 생기지 않을겁니다.

하지만 Thread2, Thread3이 돌고있다면 이는 데드락의 높은 가능성을 내재하고 있습니다.

 

이유를 말씀드리겠습니다.

Thread2에서 lock1()을 하는것과

Thread3에서 lock2()를 하는것이 동시에 일어난 일이라고 가정한다면

각각 잠금을 걸고 들어갈겁니다.

그리고 서로는 서로를 기다리기 위해서 block에 걸리게 될텐데 서로를 풀 수있는 열쇠를 서로가 가지고있습니다...

이경우엔 영원히 lock에서 빠져나오지 못하는 상황입니다.

즉 데드락입니다.

 

이 경우에는 순서가 서로 맞지 않아서 데드락에 걸려버린거구요...  우리는 이 경우만을 데드락으로 볼겁니다.

멍청하게 lock을 걸고 unlock을 안한 상태로 또 lock을 걸어버리는 그런 상황은 절대로 만들지 않을거기 때문에... 이 경우는 데드락이긴 하지만 데드락의 케이스에서 빼버리도록 하겠습니다.(이런적이 있으신 모든분들께 사과드립니다)

 

일반적으로 lock이라는 것은 하나의 리소스나 변수를 대상으로 하는 경우는 드뭅니다.

그런경우에는 데드락이 발생할 확률이 높아집니다.

하나의 로직 혹은 하나의 컨텐츠에 적용을 시켜서 동기화 잠금을 맞추는게 일반적이고 코드에서 데드락 발생 확률을 줄이는데 도움이 됩니다.

 

꼭 두개를 같이 써야겠다면 어떤식으로 해야되는가...

void Thread2()
{
    lock1();
    lock2();
    g_Counter1 = 10;
    
    if(g_Counter1==10)
    {
        g_Counter2++;
    }
    unlock2();
    unlock1();
}

네... 뭐 어쩔수없이 이렇게 순서를 맞춘 상태로 두개다 물고 들어갈 수 밖에 없을 것 같습니다.

Thread3에서는 대입 부분과 if절을 제외하고는 동일한 문장이 나올겁니다.

 

물론 꼭 저렇게 따로따로 구분해서 쓰고싶다면 동기화 객체 API중에서 Try로 시작하는 함수들이 존재하긴합니다.

하지만... 동기화가 꼭 필요한 객체의 점유권을 내가 뺏긴 상태에서 할 수있는 행동은 없기때문에... 주로 잘 사용하지는 않습니다만... 이런경우로 사용할 수  있을 것 같습니다.

 

이런 경우로 사용할 수있겠습니다...

뭐 이건 추후에 나올 spin lock이라는 개념이랑 비슷하긴 한데요...

일단은 나중에 저부분에 대한 설명을 따로 하겠습니다.

이런 경우에 try함수를 써서 저렇게 풀어 나갈 수 있다는 점을 보여드리는게 지금 목적이니까요...

우선은 이 정도 숙지한 상태에서 쓰레드로 넘어간다면 쓰레드를 하는 중간중간에 계속 동기화 객체에 대한 얘기가 나올겁니다.

지금은 우선... 동기화에 대한 개념부터 잡아가는게 우선이기 때문에 많은 부분 생략하고 설명하고 있습니다.

동기화에 대한 개념이 잡히기 시작한다면 그때부터는 실제 동기화 객체들의 예시를 가져와서 어떤식으로 작동되는지 어떤원리로 동기화가 이뤄지는지 확인해볼 계획입니다.

 

일단은 글이 너무 길어지고 다음에 적을 내용이 쓰레드와 관련된 내용이기 때문에 여기서 한번 끊고 가도록 하겠습니다.

 

후... 요새 다시 이론으로 넘어오면서 좀 코드와 동영상보다는 글 위주의 포스팅이 이뤄지고 있습니다.

그래도 아직 깊은부분은 보지 않았으니 할만하잖아요..? 지금까지 내용은 아주 베이스에 해당하는 내용이니까요...

 

일단 다음글에서는 진짜로 쓰레드에 대한 내용을 다루겠습니다.

 

그럼 오늘도 긴글 읽어주셔신 여러분들께 감사드리고 다음글에서는 더 좋은 내용으로 찾아뵙도록 하겠습니다.

그럼 안녕히계세요

320x100

'게임서버 > 멀티쓰레드 이론' 카테고리의 다른 글

Synchronization2  (0) 2022.01.21
Synchronization  (0) 2022.01.21
Thread  (0) 2022.01.20
windows scheduling  (0) 2022.01.19
Windows preemptive priority based round-robin scheduling  (0) 2022.01.18