오구의코딩모험

[Server] Game Server, Multi Thread, Atomic, Lock, Dead Lock 본문

Game/Server

[Server] Game Server, Multi Thread, Atomic, Lock, Dead Lock

오구.cpp 2025. 1. 16. 16:11
반응형

 

 

 

멀티스레드와 동기화 기법: 게임 서버와 병렬 프로그래밍

 

 

멀티스레드는 게임 서버와 같은 고성능 프로그램에서 필수적인 요소입니다.

이번 글에서는 Web Server와 Game Server의 차이, 멀티스레드와 동기화 문제, Atomic과 Mutex, Deadlock의 위험과 해결법을 중심으로 학습한 내용을 정리합니다.  

 

 




1. Web Server와 Game Server의 차이

 

Web Server (HTTP Server)
- Web Server는 질의/응답 형태로 작동합니다.
  * ex) 테이크아웃 전문 식당처럼 요청에 대한 응답을 빠르게 처리.

Game Server (TCP Server, Binary Server, Stateful Server)
- Game Server는 실시간 상호작용(Interaction)을 요구합니다.
  * ex) 일반 식당처럼 지속적인 연결을 유지하며 여러 요청을 처리.

 


 

내가 구현한 멀티스레드의 현실

 

2. 멀티스레드란?

 

멀티스레드의 개념
- 스레드프로그램의 동작 단위를 의미하며, CPU가 스레드를 배치하여 실행합니다.
- 멀티스레드는 프로그램 내 여러 스레드가 동시에 작업을 수행하며, HeapData 영역을 공유합니다.

멀티스레드의 장점과 문제점
- 장점 : 여러 스레드가 동일한 데이터를 참조하여 효율적으로 작업할 수 있습니다.
- 단점 : 동시에 동일한 데이터를 접근하면 충돌이 발생할 수 있습니다.
  * ex) 두 스레드가 같은 변수의 값을 동시에 수정하려 할 때 결과가 예측 불가능해짐.

 

 

#include <iostream>
#include <thread>

void HelloThread()
{
	cout << "Hello Thread" << endl;
}

void HelloThread_2(int32 num)
{
	cout << num << endl;
}


int main()
{
	std::thread t(HelloThread);	// 스레드 생성
    
	std::thread t2(HelloThread_2, 10);

	cout << "Hello Main" << endl;	// 메인 함수 출력

	t.join();			// 스레드가 끝날 때까지 기다린다.
    	t2.join();
}

 

C++ 스레드 생성

 - 운영체제에 스레드 생성을 요청하고 운영체제에서 처리를 해줘야 스레드가 생성된다.

 - <thread> 헤더파일의 함수를 사용하여 window/linux에 공용적으로 활용가능한 스레드 생성이 가능하다.

 - 생성된 스레드가 끝나지 않았는데 함수가 종료되면 에러가 발생한다. 따라서 join을 사용하여 생성된 스레드가 끝날 때까지 대기한다.

 - 인자가 존재하는 함수의 스레드를 생성할 때는 인자를 함께 스레드 객체에 넣어준다.

 

 

## 알고 있어야 할 스레드 함수

t.hardware_concurrency();	// CPU 코어 개수
t.get_id();			// 스레드마다 ID
t.detach();			// std::thread 객체에서 실제 쓰레드를 분리. 분리 후 정보 추출 불가
t.joinable();			// ID가 0인지 아닌지, 쓰레드 객체가 살아있는지 판별
t.join();			// 쓰레드가 끝날 때까지 기다린다.

 

## vector를 활용한 다수의 thread 호출
## 동기화 코드가 없기에 출력 순서는 뒤죽박죽이다.
    
vector<std::thread> v;

for (int32 i = 0; i < 10; i++)
	{
		v.push_back(std::thread(HelloThread_2, i));
	}

for (int32 i = 0; i < 10; i++)
	{
		if (v[i].joinable())
			v[i].join();
	}

 

int32 sum = 0;

void Add()
{
	for (int32 i = 0; i < 100'0000; i++)
	{
		//sum++;
		int32 eax = sum;
		eax = eax + 1;
		sum = eax;
	}
}

void Sub()
{
	for (int32 i = 0; i < 100'0000; i++)
	{
		//sum--;
		int32 eax = sum;
		eax = eax - 1;
		sum = eax;
	}
}

int main()
{

	Add();
	Sub();
	cout << sum << endl;	// 0 출력

	std::thread t1(Add);
	std::thread t2(Sub);

	t1.join();
	t2.join();
	cout << sum << endl;	// 값이 매번 바뀜
}

 

스레드가 공유 데이터를 사용할 때 생기는 문제점

 - t1과 t2중 나중에 완료된 쪽의 sum 값을 덮어 쓰기 때문에 값이 매번 바뀌게 된다.

 - 따라서 동기화 과정이 필요하다.


 


3. 동기화 기법: Atomic과 Mutex

 

#include <atomic>

// atomic : all-or-nothing
atomic<int32> sum = 0;

void Add()
{
	for (int32 i = 0; i < 100'0000; i++)
	{
		sum.fetch_add(1);
	}
}

void Sub()
{
	for (int32 i = 0; i < 100'0000; i++)
	{
		sum.fetch_sub(1);
	}
}

 

 

Atomic
- Atomic은 연산이 All or Nothing 상태를 유지하도록 보장합니다.
- 동기화 문제를 해결할 수 있지만, 연산이 느리다는 단점이 있습니다.

 

 

#include <mutex>

vector<int32> v;
mutex m;

void Push()
{
	for (int32 i = 0; i < 10000; i++)
	{
		m.lock();
		v.push_back(i);
		m.unlock();
	}
}

int main()
{
	std::thread t1(Push);
	std::thread t2(Push);

	t1.join();
	t2.join();

	cout << v.size() << endl;
}


Mutex
- Mutex는 자물쇠와 같은 역할을 하며, 데이터를 보호하기 위해 lock과 unlock을 사용합니다.
- Mutual Exclusion(상호배타성)을 통해 공유 데이터를 안전하게 관리합니다.

 

 

// RAII (Resource Acquisition Is Initialization)
template<typename T>
class LockGuard
{
public:
	LockGuard(T& m)
	{
		_mutex = &m;
		_mutex->lock();
	}

	~LockGuard()
	{
		_mutex->unlock();
	}

private:
	T* _mutex;

};

void Push()
{
	for (int32 i = 0; i < 10000; i++)
	{
		LockGuard<std::mutex> lockGuard(m);
		v.push_back(i);

		if (i == 5000)
		{
			break;
		}
	}
}

 

std::lock_guard<std::mutex> lockGuard(m);
std::unique_lock<std::mutex> uniqueLock(m, std::defer_lock);

 

Mutex 사용 시 주의점
1. Lock/Unlock 관리 문제
   - lock 후 unlock이 제대로 이루어지지 않으면 Deadlock이 발생할 수 있습니다.
2. RAII(Resource Acquisition Is Initialization)
   - 생명주기를 관리하기 위해 RAII를 활용하여 안전하게 unlock을 보장합니다.

 

 

 


 

4. Deadlock의 위험과 해결법

 

Deadlock이란?
- 두 스레드가 서로 다른 mutex를 lock한 뒤, 상대 mutex가 unlock되길 기다리는 상태입니다.

해결 방법
1. 순서 부여
   - Mutex마다 고유 ID를 부여하고, 순서대로 호출하여 Deadlock을 방지.
2. 사이클 추적
   - Deadlock이 발생할 가능성이 있는 사이클을 추적하여 사전에 파악.

 

 



정리

 

1. Web Server vs Game Server
   - Web Server는 간단한 질의/응답 처리를, Game Server는 실시간 상호작용과 높은 동기화 요구를 처리합니다.

2. 멀티스레드 관리
   - 데이터를 효율적으로 공유하면서도 충돌을 방지하는 것이 핵심입니다.

3. 동기화 기법 활용
   - Atomic은 간단한 연산에 적합하며, Mutex는 더 복잡한 데이터 구조에 적합합니다.

4. Deadlock 방지
   - Mutex 호출 순서를 정하거나, RAII를 활용해 lock/unlock 관리를 자동화하세요.

 

 


멀티스레드와 동기화 기법은 고성능 프로그램에서 필수적인 기술입니다.

특히, Mutex와 Atomic의 역할을 이해하고, Deadlock을 예방하는 방법을 숙지하는 것이 중요합니다.

이러한 기법을 적절히 활용하면 안정적이고 효율적인 프로그램을 개발할 수 있습니다.

반응형
Comments