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;
}
멀티쓰레드 환경에서 이렇게 ProcessSave
와 ProcessLogin
를 호출하면 어떻게 실행될까?
프로세스가 종료되지 않는 경우도 있고, 이렇게 정상적으로 실행되어 종료되는 경우도 있다.
디버깅으로 새로 생성한 스레드들이 어디서 멈췄는지 확인해보면 알 수 있다.
t1
은 ProcessSave
에서 user lock을 건 상태에서 account lock을 기다리고
t2
는 ProcessLogin
에서 account lock을 건 상태에서 user lock을 기다리고 있다.
이렇게 두 스레드가 서로 lock을 걸고 있으면서 다른 lock을 풀어주기를 기다리는 상태를 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
}