[C++과 언리얼로 만드는 MMORPG 게임 개발 시리즈] Part4: 게임 서버 - DeadLock

Jangmanbo·2023년 7월 9일
0

DeadLock
두 개 이상의 작업이 서로 상대방의 작업이 끝나기 만을 기다리고 있기 때문에 결과적으로 아무것도 완료되지 못하는 상태

DeadLock이 발생하는 경우

AccountManager: 계정 관리 (로그인 등)
UserManager: 유저 관리(유저 정보 저장, 가져오기 등)

AccountManager.h

class Account
{
	// TODO
};

class AccountManager
{
public:
	// singletone
	static AccountManager* Instance()
	{
		static AccountManager instance;
		return &instance;
	}

	Account* GetAccount(int32 id)
	{
		lock_guard<mutex> guard(_mutex);
		return nullptr;
	}
	
	void ProcessLogin();

private:
	mutex _mutex;
};

AccountManager.cpp

void AccountManager::ProcessLogin()
{
	// account lock
	lock_guard<mutex> guard(_mutex);

	// user lock
	User* user = UserManager::Instance()->GetUser(100);

	// TODO
} 

UserManager.h

class User
{
	// TODO
};

class UserManager
{
public:
	// singletone
	static UserManager* Instance()
	{
		static UserManager instance;
		return &instance;
	}

	User* GetUser(int32 id)
	{
		lock_guard<mutex> guard(_mutex);
		return nullptr;
	}

	void ProcessSave();

private:
	mutex _mutex;
};

UserManager.cpp

void UserManager::ProcessSave()
{
	// user lock
	lock_guard<mutex> guard(_mutex);

	// account lock
	Account* account = AccountManager::Instance()->GetAccount(100);

	// TODO
}

ProcessLogin에서는 account lock 후에 GetUser를 호출하며 user lock,
ProcessSave에서는 user lock 후에 GetAccount를 호출하며 account lock을 한다.


GameServer.cpp

void Func1()
{
	for (int32 i = 0; i < 1; i++)
	{
		UserManager::Instance()->ProcessSave();
	}
}

void Func2()
{
	for (int32 i = 0; i < 1; i++)
	{
		AccountManager::Instance()->ProcessLogin();
	}
}


int main()
{
	std::thread t1(Func1);
	std::thread t2(Func2);

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

	cout << "Jobs Done" << endl;
}

멀티쓰레드 환경에서 이렇게 ProcessSaveProcessLogin를 호출하면 어떻게 실행될까?

프로세스가 종료되지 않는 경우도 있고, 이렇게 정상적으로 실행되어 종료되는 경우도 있다.


프로세스가 종료되지 않은 이유

디버깅으로 새로 생성한 스레드들이 어디서 멈췄는지 확인해보면 알 수 있다.

t1ProcessSave에서 user lock을 건 상태에서 account lock을 기다리고

t2ProcessLogin에서 account lock을 건 상태에서 user lock을 기다리고 있다.

이렇게 두 스레드가 서로 lock을 걸고 있으면서 다른 lock을 풀어주기를 기다리는 상태를 deadlock(교착상태) 이라고 한다.

결론은 deadlock 상태에 빠져 두 스레드가 아무런 일도 하지 못해 프로세스가 종료되지 않은 것이다.

Deadlock에 빠지지 않는 법

lock을 거는 순서가 정해져 있으면 된다.

void UserManager::ProcessSave()
{
	// account lock
	Account* account = AccountManager::Instance()->GetAccount(100);

	// user lock
	lock_guard<mutex> guard(_mutex);	

	// TODO
}

account lock->user lock 순서대로 ProcessSave를 수정하면, 두 스레드 모두 account lock을 먼저 잡게 되므로 교착상태에 빠지지 않게 된다.

순서를 맞춰주기 위해서 실제 개발에서는 mutex에 숫자를 매기기도 한다.






코드

GameServer.cpp

#include "pch.h"
#include <iostream>
#include "CorePch.h"
#include <thread>
#include <atomic>
#include <mutex>
#include "AccountManager.h"
#include "UserManager.h"

void Func1()
{
	for (int32 i = 0; i < 100; i++)
	{
		UserManager::Instance()->ProcessSave();
	}
}

void Func2()
{
	for (int32 i = 0; i < 100; i++)
	{
		AccountManager::Instance()->ProcessLogin();
	}
}


int main()
{
	std::thread t1(Func1);
	std::thread t2(Func2);

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

	cout << "Jobs Done" << endl;
}

UserManager.h

#pragma once
#include <mutex>

class User
{
	// TODO
};

class UserManager
{
public:
	// singletone
	static UserManager* Instance()
	{
		static UserManager instance;
		return &instance;
	}

	User* GetUser(int32 id)
	{
		lock_guard<mutex> guard(_mutex);
		return nullptr;
	}

	void ProcessSave();

private:
	mutex _mutex;
};

UserManager.cpp

#include "pch.h"
#include "UserManager.h"
#include "AccountManager.h"

void UserManager::ProcessSave()
{
	// account lock
	Account* account = AccountManager::Instance()->GetAccount(100);

	// user lock
	lock_guard<mutex> guard(_mutex);	

	// TODO
}

AccountManager.h

#pragma once
#include <mutex>

class Account
{
	// TODO
};

class AccountManager
{
public:
	// singletone
	static AccountManager* Instance()
	{
		static AccountManager instance;
		return &instance;
	}

	Account* GetAccount(int32 id)
	{
		lock_guard<mutex> guard(_mutex);
		return nullptr;
	}
	
	void ProcessLogin();

private:
	mutex _mutex;
};

AccountManager.cpp

#include "pch.h"
#include "AccountManager.h"
#include "UserManager.h"

void AccountManager::ProcessLogin()
{
	// account lock
	lock_guard<mutex> guard(_mutex);

	// user lock
	User* user = UserManager::Instance()->GetUser(100);

	// TODO
} 

0개의 댓글