데드락을 이해하기 위해선 락(Lock)의 원리를 이해해야 합니다.
락을 거는 이유는 쉽게 설명하면 다음과 같습니다.

A와 B가 버스표를 예매합니다.
버스표 예매 과정은, 목적지 선택 -> 시간/등급 선택 -> 좌석 선택 -> 결제 입니다.
A와 B가 동시에 서울 10시 우등을 선택하고, 둘다 같은 좌석 20번으로 선택하고 결제합니다. 
A와 B가 버스를 탔는데... 헐!? 표가 같네요 -.-
이러면 안되죠...?

때문에 좌석선택 이후부터 결제가 종료될때까지 서울 10시 우등의 20번 좌석은 락을 겁니다.
A가 변심하여 20번 좌석을 택하지 않고 21번 좌석으로 변경한다면 B는 20번 좌석을 선택할 수 있고, 그렇지 않으면 B는 20번 좌석이 아닌 다른 좌석을 선택해야 하죠.
( 사실 이 예제는 DB의 트랜젝션에 더 어울리는 예제지만, 트랜젝션도 락의 일종이라고 보면 됩니다. 오히려 이 예제를 통해 DB 트랜젝션에 대한 이해도 동시에 될 수 있습니다 )

통칭 락이라고 부르는 종류에는 대표적으로 두가지가 있습니다.

뮤텍스(mutex) 그리고 세마포어(semaphore)인데요. ( 이외에도 객체락인 atomic, interlocked 등 여러가지 기술적인 부분이 더 존재하긴 합니다만 생략합니다 )

뮤텍스는 1:1 락, 세마포어는 1:N 락이라고 생각하시면 됩니다.
뮤텍스는 오직 하나만 접근이 가능, 세마포어는 지정된 수만큼 접근이 가능합니다.
뮤텍스는 위 예제로 이해하시면 되겠고, 세마포어는 성질이 다릅니다.

예를들어 명품백 매장이 있고, 매장안에는 5명의 직원이 있습니다. 저도 정확히는 모르지만, 아마 손님과 직원이 1:1로 제품 설명을 하고 고객관리를 하는듯 하는데, 이 경우가 세마포어의 예랑 밀접하다고 보시면 됩니다.
밖에 줄을 주르륵 서있는건 매장안에 출입가능한 손님수는 5명이죠.
이때 입구에서 입장을 안내하는 직원이 세마포어인겁니다.

이정도면 이해가 되셨으리라 믿고요,

데드락 현상은 락이 풀리지 않는 현상을 말합니다. ( 락이 풀리지 않으면 로직이 정상작동하지 않고, 로직이 정상작동하지 않으면 프로그램이 죽었음을 의미하기 때문에 dead-lock이라고 합니다 )

첫번째 예제의 버스 예매를 예로 들어보면, 
* 예 1
1. 좌석 선택후 결제 화면으로 넘어갈때 락이 걸립니다.
2. 결제화면이 뜨고, 결제를 진행합니다.
3-1. 결제가 성공하고 락을 풉니다. ( 정상 종료 )
3-2. 결제가 실패하여 안내 메세지를 뿌리고 처음 화면으로 돌아갑니다. ( 잘못된 프로그래밍 )
4. 3-2 이후 대기하고 있던 좌석선택 대기자는 데드락에 빠집니다.

* 예 2
1. 좌석 선택 전체를 락을 겁니다. ( 잘못된 프로그래밍 )
2. 좌석 선택후 결제 화면으로 넘어갑니다.
3. 목적지, 시간, 등급, 좌석 정보를 사용자에게 다시 확인시켜주기 위해 다시한번 데이터를 조회합니다.
4. 좌석 정보를 조회하려는데 이미 락이 걸려있습니다. ( 대기탑니다 )
5. 좌석 선택을 대기타는 사용자도, 결제하려는 사용자도 데드락에 빠집니다.

대표적으로 많이 실수하는 예제 두가지를 들었습니다.
1번의 실수가 특히! 더 많기 때문에 lock guard를 사용하는 등의 기법들이 많이 등장했죠.
2번의 실수는 프로그래머 실수이고, 되도록 발생하지 말아야 하지만, 이 또한 recursive-lock이라는 개념(같은 쓰레드 같은 락 객체의 경우 대기타지 않도록 하는 lock)이 등장하여 프로그래머의 실수를 예방해주지만 성능에 영향이 있습니다.

마지막으로 기억에 남는 질문과 답변글중에, concurrent hash map을 사용하던걸 fast map으로 바꿨더니 데드락 현상이 사라졌다는 글이 있었습니다.
결론만 먼저 말씀드리면 매우 위험합니다.

예를들어 fast_map을 이용해 자판기(시장)를 이용한다고 해봅니다.
A 유저가 축데이를 1아덴에 올려놓습니다.
B, C 유저가 동시에 축데이를 구매합니다.
락이 없으므로 A유저는 1아덴, B, C유저는 축데이 각각 1장씩 얻습니다.

shop => fast_map

0. if( A.shop[ 축데이 ].갯수 > 0 ) - B, C 동시 통과
1. B.아데나 -= A.shop[ 축데이 ].가격;
1. C.아데나 -= A.shop[ 축데이 ].가격;
2. A.shop[ 축데이 ].갯수--; ( A.shop[ 축데이 ].갯수 는 0 이하로 됨 )
3. B.인벤토리.add( 축데이 )
3. C.인벤토리.add( 축데이 )
4. B, C 로직 종료

이런 현상이 발생하죠.

concurrent_hash_map 을 씁니다.
0. if( A.shop[ 축데이 ].갯수 > 0 ) - B통과 C 대기 ( 축데이 객체 락 )
1. B.아데나 -= A.shop[ 축데이 ].가격;
2. A.shop[ 축데이 ].갯수--; ( A.shop[ 축데이 ].갯수 는 0 이하로 됨 )
3. B.인벤토리.add( 축데이 )
4. B 로직 종료 - 축데이 객체 락 해제
5. 축데이 갯수 0개
6. C 로직 종료 

즉.. 락을 걸지 않았을때 나타나는 버그는 데드락보다 훨씬 위험하고 잡기 어렵습니다.
참고하시고 근본적인 원인을 해결하는 방법을 생각하는 것을 권유드립니다.

* 요약
1. 락은 데이터 무결성 위해 꼭 필요하다 ( 멀티쓰레드 환경 )
2. 락이 무기한 잠기어 프로그램이 정상작동하지 않는 상태를 데드락이라고 한다.
3. 데드락 상태에 빠지지 않도록 설계하고 코딩하는것이 제일 중요하다.


+ Recent posts