dh_0e

[C++/Game Server] Lock-Based Stack/Queue (+delete, IN/OUT) 본문

C++/Game Server

[C++/Game Server] Lock-Based Stack/Queue (+delete, IN/OUT)

dh_0e 2026. 2. 5. 16:27

Mutex를 활용한 Stack과 Queue

<ConcurrentStack.h>

#include<mutex>

template<typename T>
class LockStack
{
public:
	LockStack() {}

	// 1. 복사 생성자 삭제: "나랑 똑같은 놈 복사해서 새로 만들지 마!"
	// 복사 생성자 금지 : LockStack s2 = s1; 처럼 새로운 객체를 복사해서 만들려고 하면 컴파일 에러 발생.
	LockStack(const LockStack&) = delete;
	// 2. 복사 대입 연산자 삭제: "이미 있는 놈한테 내 내용물 복사해주기 싫어!"
	// 복사 대입 연산자 금지: s2 = s1; 처럼 이미 만들어진 객체에 값을 덮어쓰려 해도 컴파일 에러 발생.
	LockStack& operator=(const LockStack&) = delete;

	void Push(T value)
	{
		lock_guard<mutex> lock(_mutex);
		_stack.push(std::move(value)); // std::move로 주솟값 빼앗아오기(얕은 복사)로 시간 절약 >> value에는 빈 껍데기만 남음
		_condVar.notify_one();
	}

	bool TryPop(T& value)
	{
		lock_guard<mutex> lock(_mutex);
		if (_stack.empty())return false;

		value = std::move(_stack.top());
		_stack.pop(); // C++ stack에서 pop과 top이 분리된 이유: pop하는 도중 복사 과정에서의 exception(충돌)을 방지하기 위해서
		return true;
	}

	// empty를 체크하고 나서 Pop을 해도 사실 그 사이에 값이 바뀌었을지 모르기 때문에 의미가 딱히 없음
	bool Empty()
	{
		lock_guard<mutex> lock(_mutex);
		return _stack.empty();
	}

	// Pop할 때 데이터가 없다면 끝나는게 아니라 데이터가 들어올 때까지 기다리는 버전
	void WaitPop(T& value)
	{
		unique_lock<mutex> lock(_mutex); // 조건 변수 사용 시엔 lock_guard가 아닌 unique_lock 사용
		_condVar.wait(lock, [this] {return !_stack.empty(); });
		value = std::move(_stack.top());
		_stack.pop();
	}

private:
	stack<T> _stack;
	mutex _mutex;
	condition_variable _condVar;
};

 

= delete

  • C++11 이전에 특정 함수의 기능을 막고 싶을 때, 해당 함수를 private 영역에 선언만 하고 구현은 하지 않는 꼼수를 썼었음
  • 이를 public 영역에 delete와 함께 당당하게 써서 설계 의도부터 금지된 것임을 컴파일러에게 알려주기 위해 C++11부터 사용됨
  • 주로 사용되는 상황
    1. 복사가 불가능한 자원을 관리할 때
      • Mutex나 File Handle같은 하나만 존재해야 하는 물리적/논리적 자원을 복사한다는 것은 논리적으로 말이 안되기 때문
    2. 싱글톤(Singleton) 패턴을 만들 때
      • 마찬가지로 프로그램 전체에 단 하나의 인객체만 존재해야 하는 클래스를 만들 때 실수로라도 복제되는 것을 막기 위해 사용
    3. 원치 않는 형변환을 막을 때
      • 형변환을 막기 위해 생성자가 아닌 일반 함수에 사용
      • ex) 정수만 받아야 하는 함수에 실수가 들어오는 것을 막고 싶을 때 다음과 같이 작성하여 막을 수 있음 
        void OnlyInt(int a) { /* ... */ }
        void OnlyInt(double) = delete; // 실수(double)가 들어오면 컴파일 에러!

 

<ConcurrentQueue.h>

#include <mutex>

template<typename T>
class LockQueue
{
public:
	LockQueue() {}

	LockQueue(const LockQueue&) = delete;
	LockQueue& operator=(const LockQueue&) = delete;

	void Push(T value)
	{
		lock_guard<mutex> lock(_mutex);
		_queue.push(std::move(value));
		_condVar.notify_one();
	}

	bool TryPop(T& value)
	{
		lock_guard<mutex> lock(_mutex);
		if (_queue.empty())return false;

		value = std::move(_queue.front());
		_queue.pop(); 
        	// C++ stack에서 pop과 top이 분리된 이유
		// >> pop하는 도중 복사 과정에서의 exception(충돌)을 방지하기 위해서
		return true;
	}

	void WaitPop(T& value)
	{
		unique_lock<mutex> lock(_mutex);
		_condVar.wait(lock, [this] {return !_queue.empty(); });
		value = std::move(_queue.front());
		_queue.pop();
	}

private:
	queue<T> _queue;
	mutex _mutex;
	condition_variable _condVar;

};

 

 

<Main.cpp>

#include "ConcurrentQueue.h"
#include "ConcurrentStack.h"

LockStack<int32> st;
LockQueue<int32> que;

void Push()
{
	while (1) 
	{
		int32 value = rand() % 100;
		que.Push(value);
		this_thread::sleep_for(10ms);
	}
}

void Pop()
{
	while (1)
	{
		int32 data = 0;
		que.WaitPop(OUT data); // OUT: 출력용 데이터라는 뜻 #define OUT 으로 빈껍데기로 저장되어 있음
		cout << data << endl;
	}
}

int main()
{
	thread t1(Push);
	thread t2(Pop);
	thread t3(Pop);

	t1.join();
	t2.join();
	t3.join();
	
	return 0;
}

 

IN/OUT/INOUT 메크로

  • 아무런 기능이 없는 '빈 껍데기'로 해당 매개변수가 수정되는지, 읽기만 하는지 등 가독성을 위해 이를 표현하는 약속(관례)
#define IN      /* 입력용 (보통 const 참조) */
#define OUT     /* 출력용 (값 수정됨) */
#define INOUT   /* 입력받아서 수정까지 함 */

// 호출할 때
que.WaitPop(OUT data);
// data 이 변수 안에서 값 바뀐다!
  • C# 같은 언어엔 out이라는 키워드가 실제로 문법으로 존재
  • 보통 Windows 헤더 파일에 존재