Lock 기초

CJB_ny·2022년 2월 9일
0

Unity_Server

목록 보기
10/55
post-thumbnail

지난시간에 멀티 쓰레드 환경에서 일어날 수 있는

Race Condition라는 "경합 조건"에 대해서 살펴 보았고

InterLocked 계열의 함수를 사용해서 ref _number를 처리를 하는 부분까지도 알아 보았다.

InterLocked 계열이 성능도 빠르고 굉장히 우수하기는 한데

사실은 조금 치명적인 단점이 있기는 하다.

이런식으로 거의 정수만 사용할 수 있는 단점이 있다.

예를들어 나중에 우리가 멀티 쓰레드 코드를 짤때는

이렇게 number++을 하는 단순한 작업이 아니라

이런곳에 밑에 몇십줄 혹은 몇백줄 짜리 코드가 들어 갈 수도 있는데

그것을 쓰레드마다 InterLocked로 모든 것을 만들 수 는 없다.

그래서

이런식으로 블록을 주어서

이 블록 안에있는 것들은 하나의 쓰레드에서만 실행이 되어야 하고

실행되는 동안 다른 애들(쓰레드들은) 기다려라 할 수 있는

그런 "도구"가 당연히 필요하게 될 것이다.

오늘은 그래서 그 부분에 대해서 알아 볼 것이다.

InterLocked는 "자주" 사용되기는 할 것이다.

이녀석은 잠시 삭제를 하도록 하고 원래 버젼으로 돌아 가보도록 하자.

이 상태인데

지난번에 얘기 했던 내용을 다시한번 복습을 해보면은

지금 이 코드는 눈으로 볼 때는 한줄 짜리 코드이지만

컴퓨터가 나중에 실행을 할 때는 세단계에 걸쳐서 일어나는 것이라고 했었다.

이렇게 전역변수에 있는 즉, 이 값의 메모리에 있는 값을

"레지스터"에 가지고 오고,

레지스터를 이용해서 1을 더한다음에

더한 값을 다시 static int number의 메모리 값에 접근을 해가지고

저장하는 부분까지 세단계로 나뉜다고 했었다.

그런데

사실, 멀티쓰레드에서 어떤 메모리에 있는 값을 읽는 것은 별 문제가 되지 않는다.

아까말한 세단계를 의사코드로 잠깐 표현하면 이런식인데
(temp에 number를 넣는 것은 임시 변수 레지스터에 들어간다고 잠깐 언급을 했었다)

여기서 중요한것은 뭐냐하면은...

            int temp = number;

이런식으로 number를 가지고 오는 것은 별로 문제가 되지 않는다.

왜냐면 애당초 값이 바뀌는 것이 아니기때문에

다른데서도 값을 가져오는 것은 문제가 되지 않지만 진짜

문제가 되는 부분이 무엇이냐하면은

어떤(다른) 쓰레드에서

            number = temp;

이런식으로 쓰기(write) 시작할 때,

즉, 다른 곳에서도 write하는 작업이 들어가기 시작하면은

            int temp = number;

이런식으로 값을 가져오는 부분도 문제가 된다는 부분이다.

그래서 이런식으로 동시다발적 으로 접근을하면 문제가 되는 그런 코드들을

사실은, 또 다른 특별한 용어가 있는데

그것을 "크리티컬 섹션" 이라고 한다.

1. "크리티컬 섹션"

== "임계 영역" 이라는 부분이고

그 임계 영역을 해결하기 위한 방법중 하나가

"InterLocked" 였었다!

그런데, 오늘 해볼것은 InterLocked를 해볼 것이 아니라

아예 그냥 선을 그어 줄 것이다.

선을 그으면 내가 먼저 점유를 했으니 다른애들은 얼씬도 하지 말아라! 라는

그런 뉘앙스로

코드를 작업을 해볼 것이다.

일단은 첫 준비물은 오브젝트를 만들어야 한다.

이런식으로 만들어 주자.

object타입이다 보니까 그냥 존재만 있다 뿐이지

사실 이녀석을 어떻게 뭐 뭔가를 저장하는 용도로 사용하지는 않을 것이다.

이렇게 static을 붙여 주어야지만 Thread 안에서 사용할 수 있으니까 붙여 주고

그리고 이제 _obj를 사용하는 방법은 사실 여러가지가 있는데

가장 기초적인 것부터 해보도록 하자.

모니터.Enter를 먼저 해주면 된다.

먼저 이렇게 해주고

여기선 반대로

Exit를 해주면된다.

그리고

여기다가도 똑같은 것을 넣어 주면 될 것이다.

그리고 이게 어떤 코드인지는 모르겠지만 한번 실행을 해보면

0이라는 값이 일단 잘 나온다.

그러면 일단 해결하는 것까지는 좋았는데

어떤 역할을 하는 것인지 궁금하지 않을 수 없는데

이것을 쉽게 비유를 하자면 "화장실"에 비유를 하면된다.

만약 비행기를 타면은 비행기 안에 화장실을 보면은

1인용으로 밖에 사용할 수 밖에 없다.

그리고 화장실 안에 들어가면 문을 잠궈야 한다.

이렇게 문을 잠구었는데

다른애들이 들어 올려고하는데

이런식으로 누가 문을 잠구어서 다른 사람이 들어올려고 하면은

이녀석은 하염없이 여기서 대기를 해야한다.

문을 이렇게 잠구었으면

이 안은 이제 "안정권"이라고 생각하면 된다.

그리고 볼일을 다 봤으면

Exit를 해가지고 문을 풀어주게 된다.

이런 느낌이 될 것이다.

그래서

_obj는 문 열쇠같은 그런 개체가 되는 것이고

이렇게 잠구었으면 다른애들은 접근을 못하는 것이다.

그런데

Thread_1에서 먼저 못잠구고

Thread_2에서 먼저 잠구고 들어 갔다라고 하면은

Thread_1의 Monitor.Enter녀석은 잠시 대기를 해야 될 것이다.

그래서 당장 들어가지는 못하고 조금 기다려야되는 그런 상황이 발생하게 될 것이다.

그래서 문을 잠구고 있다라고 표현을 하니까 굉장히 직관 적인데

이런 상황을

2. 상호 배제 Mutual Exclusive

=> 서로 배제 한다. 이기주의적인 것이다.

즉, 나만 사용할 것이다.

다른 사람들은 얼씬도 하지 말아라! 라는 그런 개념이다.

그래서 이런식으로 사용을 하면 이 안은 나만 사용을 하겠다! 라는 그런 의미가 될 것이다.

그래서 아까 문제가 되었던 것은

number++ 를 하는 동안에

number-- 를 동시에 실행을 했기 때문에 문제가 되었던 것이데,

여기서 밑줄친 부분은 이 작업이 끝날 때까지 다른 녀석들이 건드리지 못한다는 것을

"보장"받을 수 있다.

다른애들도 똑같이 이런식으로 Lock을 걸어서 작업을 해주었기 때문에


그래서 이 블록안에 있는 부분은

사실상 "싱글 쓰레드"라고 가정을 하고 프로그래밍을 해도 된다는 얘기이다.

그래가지고 만약

이 안에 블록이 굉장히 길어진다고 하면은

아까 말한 InterLocked로는 구현을 할 수 없으니까

그래서 어떤식으로든 이렇게 범위를 지정을 해서 통크게 잠금을 해주는 상황으로 왔다.

그리고 사실 이런식으로 잠구고 풀어주고 하는 부분이

언어마다 다 이런것이 마련이 되어있다.

예를 들어 c++로 가면은

이런식으로 window전용 운영체제 에서는

CriticalSection 라는 이름으로 존재를 하고

C++표준으로 가면은 std::mutex 라는 이름으로 존재를 하고있다.

아무튼 조금씩 작동하는 방법은 다를지 언정

"인터페이스"는 어쨋든

이런식으로 잠구고 풀어주는 방식으로 되어있다.

그런데 이런식으로 Enter, Exit를 사용하게 될 경우 문제가 하나 있기는 하다.

"관리"를 하기 어려워 진다는 문제점이 있다.

예를 들면,

이런식으로 하면 ㄱㅊ은데

만약 실수로

이런식으로 return 을 대려버리게 되면

밑에 Exit를 안해주게 되는 것이다.

이게 문을 잠구었는데 문을 풀어주지 않고 나가게 되면은

어떤일이 일어 날지 궁금하다..

그래서 코드를 실행을 해보면은

이제 뭔가 끝나지가 않는 것을 볼 수 있다.

그래서 결국에는 문제가 되는 부분은 뭐였냐하면은

화장실로 다시 비유를 해보면

비행기 화장실을 이용하기위해서 누군가가

이렇게 문을 잠구었는데

나오면서 이렇게 문을 열고 나가줘야하는데

안에서 잠들어 버린 상태에다.

문을 잠구고 안에 계속 가만히 있는 상태가 되어버린 것이다.

이렇게 되면 문이 영영 열리지 않게 되는 것이다.

그렇다는 것은

다른 사람들이 이렇게 들어가기 위해 대기를 하고있는데

얘도 빠져나가지 않고 무한대로 대기를 하게 되는 것이다.

그러면은 결국

이녀석은

return;을해서 일을 끝냈기 때문에 다른일을 신나게 하러 갈텐데

Thread_2같은 경우에는

여기서 하염없이 기다리고 있으니까

사실 "먹통"이 되는 그런 문제가 발생을 하는 것이다.

그리고 이런 상황을 유식한 표현으로 하면은

"데드 락" 상황이라고 한다.

3. Dead Lock

Lock이라는게 아까

우리가 사용한 _obj와같은 자물쇠의 역할을 한다고 했었다.

그런데 DeadLock 즉, 죽은 자물쇠이다.

더 이상 사용할 수 없는 자물쇠이다 라는 느낌으로 이해를 하면 될 거같고

그래서 결국

Enter, Exit는 이런식으로 짝을 꼭 맞춰 줘야된다는 얘기인데

우리가 진짜 조심조심 해가지고 return 을 때리기 전에

Enter 하고 Exit의 짝을 다 맞춰 줬다고 가정을 해보자.

만약

number가 특정 값이 되었을 때 종료를 하고싶을 때

유심히 보니까 여기 if문 안에 Exit를 안넣어 줬네?

그래서

이런식으로 하나씩 추가를 하는게 한가지 방법이 될 수는 있는데

벌써부터 코드가 더러워 지기 시작했다!!

이런식으로 하나하나씩 수동으로 넣어주는게 여간 불편한게 아닌데다가

사실은 더 "큰" 문제는 뭐냐하면은

이런식으로 명시적으로 return 을 때리는 경우에서

우리가

                    Monitor.Exit(_obj); 

를 챙길 수 있으면 다행인데,

그게 아니라 뭔가 exception이 발생해가지고

우리가 예측하지 못한 곳에서 빠져나간다고 가정을 해보면

예를 들면 조금 극단적인 경우이기는 하지만

number를 0으로 나누는 상황같은게 있다면 바로 나갈 수도 있다.

만약 그런경우가 있다면 Exit가 챙겨지지 않을 것이다.

그래서 조금 일반적인 상황에서 Enter, Exit를 사용하고 싶으면은

Try Catch구문을 사용을 해가지고

try를 한다음에 finally를 이용하면은

이런식으로 try에서 뭔가를 해보는데

exception이 일어났건 말건

무조건 여기는 한번이 실행이 될테니까

이렇게 챙겨주게 된다면

해결방법이 될 수는 있다.

그래서 이렇게 되면은 만약,

여기서 return 을 때리게 된다고 가정을 해도

어쨋든 이

finally는 무조건 한번은 실행이 될 것이다.

실제로 잘되는 지 확인을 해보기 위해서

여기다가 break Point를 잡고 실행을 해보면

여기에서 딱 걸리는 것을 볼 수 있다.

참고로 return 을 때리고 바로 오는지 안오는지 궁금하기 때문에

return 에도 breakPoint를 잡고 실행을 해보면은

이렇게 걸리고 다음에 실행될 코드 보면은

이렇게 Finally안에 들어옴!

그다음에 Exit를 실행을 해주는 모습이다!

그래서

이렇게 try, finally를 이용하면 그나마

아까와 같이 짝을 안 맞추는 문제

문을 잠구고 안풀어준 상태에서 도망을 가는 상태를 예방을 할 수는 있기는 한데,

그럼에도 불구하고 이렇게 try finally를 일일히 넣어주는 것이 여간 번거로운 일이 아닐 수 없다.

그래서 이렇게 직접 Monitor.Enter를 사용하는 경우는 본적이 없고,

대부분의 경우에는

4. lock 키워드

"lock" 이라는 키워드를 사용하게 될 것이다!

이런식으로 사용을 하게 되는데

lock이 사실상 하게되는 역할은

try, finally와 똑같은 역할을 하게되기는 하는데

lock이라는 녀석도 내부 구현은 Monitor.Enter로 이루어져있다.

그런데 try finally를 훨씬더 편리하게

lock(_obj)로 만들어 줄 수있다는 점이 있다.

그래서 이런식으로 가능하다!

그리고 _obj는 이름을 onj로 하니까 너무 성의가 없기는 한데

이녀석은 "자물쇠"의 개념이 되는 것이고

이렇게 자물쇠가 열려있다면,

이렇게 일단 "잠구 겠다!" 이고,

이렇게 먼저 잠구고 들어가겠다! 라는 의미이고

안에서

number++을 한다음에

알아서 다시 열어준다는 개념이 된다!

그렇다면

이부분을 Thread_2에서도 똑같이 해줄 수 있을 것이다.

이렇게 똑같이 맞춰주고 실행을 해보면

아까와 같이 0이 잘 나오는 것을 확인 할 수 있다.

그래서

DeadLock 이 발생하는 상황을 조금이라도 줄일 수 있는 방법이니까

이제부터 lock 키워드를 사용하는 방식으로 진행하면 될 것이다.

-끝-

profile
https://cjbworld.tistory.com/ <- 이사중

0개의 댓글