일반적으로 아래 처럼 한쪽에서는 queue 에 push 해주고

한쪽에서는 pop 해주는 consumer- producer 코드에서

 

queue 에서 데이터를 빼낼때 보통은 아래의 3단계를 거치게 된다. 

 

1. 큐가 비었는지 확인하고

2. 큐.front() 값을 저장하고

3. q를 pop() 해준다.

 

 

이 코드를 곧이 곧대로 실행 시키면 Crash가 날것이다.

공용으로 사용하는 q에 동시에 접근하려고 하기 때문이다.

 

----------------------------- 23.08.16 추가

빠르게 복습을 하다가 해당 부분에 Crash 나는 이유를 명확히 생각해봤는데 순간 떠오르지가 않았다.

그냥 강의에서 Crash 난다더라~ 하고 넘어간거 같은데.. 이런식으로 대충 넘겼던게 많은거 같다..


다시 곰곰히 생각해보니 이유를 알 것 같다.

결론부터 말하면 강의 내용이 잘 못 되었다고 생각한다. 해당 부분은 Crash가 나지 않는게 당연하다.

처음 t1,t2 쓰레드만 생성했을때는 강의에서 Crash가 날거라고 생각했는데 실제로는 나지 않았다.
-> 이 부분을 강의에서는 sleep_for(10ms) 해준거 때문에 생각보다 경합이 잘 일어나지 않아서 Crash가 일어나지 않았다고 했다.

 

그렇지만 아닌거 같다.
Crash가 나지 않은 이유는 Crash가 날 곳이 없기 때문이다.

[Lock 기초 강의] 편에서 Vector에 쓰레드 두개가 계속 돌면서 Push 할때 Crash가 나는 이유에 대해서 배웠다.
Capacity를 초과하면 기존의 메모리를 지우고 큰 영역을 만들어서 기존 내용을 복사하는게 Vector가 작동하는 방식인데

순간적으로 동시에 기존의 메모리를 지우려고 할때 어는 한 쓰레드가 이미 지운 메모리를 조금 늦게 동작한 쓰레드에서 한번더 지우려는 double-free 문제가 발생하기 때문이다.

그리고 그런걸 방지하기 위해 reserve를 아예 엄청 크게 잡아놓는다고 해도 최종적으로 2만개가 vector에 들어가야하는데 그 보다 적은 수가 들어가있는걸 확인 할 수 있다. 이건 어느 한 쓰레드가 push_back을 하려고 할때 운이 나쁘게도 다른쪽 쓰레드에서도 동시에 그 index에 push_back을 하려고해서 덮어씌어지는 문제가 발생하기 때문이다.
( 이 부분은 생각이 안나면 강의를 다시 들어보자..) 

쓸데없는 얘기를 한거 같지만.. 다시 돌아가서 위의 queue 는 이러한 crash 날 부분이 없었기 때문에 Crash가 나지 않았던거라는 말을 이리도 길게했다..

강의에서도 나왔지만 아래 처럼 Pop 하는 쓰레드를 1개 더 늘리면 Crash가 나는데 이는 이유가 너무 자명하다!

queue가 empty 인지 체크하는 부분이 있는데,

Pop을 하는 두 쓰레드 중에서 어느 한 쓰레드가 empty가 아니라고 생각해서 q.front()를 해주고 q.pop()을 하려는데

운이 나쁘게도 다른 쓰레드에서 이미 pop()을 해버려서 빈 Queue에 pop()을 한 꼴이라 Crash가 나는것이다.


----------------------------- 23.08.16 추가 끝

 

그래서 우리는 지끔까지 배운대로

공용으로 사용하는 데이터에

 

mutex를 걸어서 사용하는것에 대해서 알아봤다.

 

물론 그렇게 사용해도 괜찮지만

 

queue나 stack 은 일반적으로 자주 사용하기 때문에

queue 나 stack 자체에 lock을 넣어놔서 하나의 클래스로 이용하면 어떨까 싶다. 

 

특히나 게임을 만들때는 stack 보다는 queue 자료구조를 많이 사용한다.

 

예를 들어, 먼저 도착한 패킷을 먼저 처리해주는게 이치에 맞기 때문이다.

 

결론 : queue 에다가 mutex 기능을 합친 클래스를 하나 만들어보자.

 


 

Main 필터 아래에 ConcurrentQueue,ConcurrenStack 을 만들어주고

 

mutex를 사용할것이기 때문에 .h 파일에다가 mutex를 include 해준다.

stack 자료구조를 처음부터 구현해도 되고,

stack 과 같은 이미 stl 에서 제공하는 컨테이너를 래핑해서 사용해도 된다.

 

lock을 걸기 위해 mutex도 활용해볼거니깐 mutex도 멤버 변수로 가지고 있는다.

 

 

강의에 흥미로운 질문이 있어서 가져와봤다. 

 혹시 모르니깐 복사하는 시도는 전부 막아줘보자

 

핵심 기능은 어떤 data를 Push해주는것이기 때문에 

 

lock_guard<mutex> 를 이용해서 잠금을 해준 다음에 원래 사용하던 스택에다가 data를 넣어준다.

이때 이왕이면 std::move() 를 통해 더 빠른 이동을 할 수 있는 애들이면 이동을 시켜준다. 

그래서 왼값 참조, 오른값 참조 버전 Push를 둘다 만들어 보았다 .

그리고 참고로 Value 값을 받았는데도 0 copy가 일어난다. 강의 중 쓴 아래의 코드는 0 copy 가 일어난다.

const T& data 를 인자로 받는 왼값 참조 버전 Push

참고로 const 를 썻기 때문에 move 연산자로 이동생성 될지 모르겠다 .

* 6.25 추가) const로 하면 안된다. 이동생성할때 기본 data를 포인터든 값이던 null 로 바꿔주는데 const 붙어있으면 그런 짓을 못하기 때문에 이동생성자 호출 안된다.

 

 


 

그리고 pop 을 해줄건데 

일반적으로 pop은 empty 상태인지 먼저 체크한다. 

 

 

그런데 사실 멀티 쓰레드 환경에서는 Empty 체크한 다음에 pop() 을 하는게 큰 의미가 없다.

 

empty() 를 체크해서 데이터가 있네 하고 pop() 을 하려는 순간 다른 쓰레드가 낑겨와서 데이터를 선수처서 꺼내올 수 있다는 소리이다.

그래서 사실상 멀티쓰레드에서는 empty 라는게 크게 의미가 없다. 

물론 제공해줘도 나쁘지 않겠지만.

 

어찌 됐건 pop() 을 할때 empty를 검사해서 비지 않은 상태일때 pop 을 하는 개념이 아니라는거다 

여기서 개념이 완전히 달라진다 .

 

그래서 그냥 pop 을 할때 진짜로 pop을 성공했는지 안했는지 bool을 리턴하고 데이터는 레퍼런스로 꺼내가는 함수를 만들어 보는걸로 하자.

마찬가지로 mutex를 걸어주고,.

만약에 stack이 empty면 데이터 없다고 false 를 리턴해주고 있으면 꺼낸다음에  true 를 해준다.

 

c# 이나 다른 언어에서는 pop() 을 할때 pop() 하면 데이터가 같이 출력되는데 c++은 왜 안되냐면 
FM 방식이긴 한데 데이터를 한번에 꺼내오는 순간에 혹시라도 크래쉬가 나는 경우도 생길 수 있다는것이다.

아래에서 value = m_stack.pop() 이라고 한다면 value로 복사 과정에서 메모리가 부족해서 exception이 일어난다면

기존 스택이나 큐 자료구조가 와장창 깨지게 된다. 그런걸 막기위해서 애당초 두단계를 거치게 해놨다.

 

그런데 사실 게임에서는 진짜로 데이터를 꺼내다가 데이터가 고갈되서 뻗게 됐다면 차라리 그냥 뻗게 냅두는게 낫다. 

언젠간 다른부분에서도 연쇄작용으로 크래쉬 날거기 때문에 차라리 바로 크래쉬가 나는게 좋다.  

 

그래서 굳이 pop()을 exception 처리를 하는게 아니라 

 

value = std::move(m_stack.top());

 

로 한번에 꺼낼 수 있도록 구성해봤다. 

 


그리고 main 함수에서 while(1) 로 TryPop() 함수를 돌리면서 

데이터가 없으면 계속 해서 시도하게끔 하다가 데이터가 있으면 있네 하고 뽑아 오게 할건데

 

데이터가 없는데도 계속 while TryPop 을 시도하는건 비효율적이기 때문에

 

우리는 이전에 배운 우아한 방법인

conditional variable 을 사용 할 것이다. 

 

conditional varibale 을 제공하는 기다려주는 버전의 Pop 을 하나 더 만들어보자. 

 

우선 Push 에서 기다리고 있는 애를 하나 깨워주고 .

 

아래 처럼 만들어주면

lock 을 잡아서 들어갔다가 조건을 만족하지 않으면 lock을 풀어주고 잠들어 버리고 

 

그러다 다시 notify 가 와서 ( signal ) 이 와서 깨워주면 다시 일어나서 lock 을 잡고 똑같이 실행한다.

 

그러면 무한정으로 돌면서 TryPop을 할 필요 없이 우아하게 데이터를 꺼내갈 수 있다. 

 

 

 


똑같이 Queue 도 비슷하게 만들어보자.

 

 

그리고 이런걸 사용해보기 위해서 

 

 

 

+ Recent posts