서버/멀티쓰레드 프로그래밍

DeadLock을 Debug단계에서 추적하기

season97 2025. 6. 4. 17:08
728x90
반응형

DeadLockProfiler 설계 및 적용

  • 멀티스레드 환경에서 서로 다른 스레드가 서로의 락을 기다리는 상황이 발생하면, 프로그램은 멈춘다
  • 디버그 단계에서 이를 미리 탐지하기 위한 코드

h

#pragma once
#include <stack>
#include <map>
#include <vector>

/*--------------------
	DeadLockProfiler -> 디버그 모드에서만 작동
---------------------*/

class DeadLockProfiler
{
public:
	void PushLock(const char* name); // 어떤 락을 사용하고있는지를 받아주는 name
	void PopLock(const char* name);
	void CheckCycle(); //사이클 체크

private:
	void Dfs(int32 here);

private:
	unordered_map<const char*, int32>	_nameToId;
	unordered_map<int32, const char*>	_idToName;
	stack<int32>						_lockStack;
	map<int32, set<int32>>				_lockHistory;
	Mutex _lock;


private:
	vector<int32>	_discoveredOrder; // 노드가 발견된 순서를 기록하는 배열
	int32			_discoveredCount = 0; // 노드가 발견된 순서
	vector<bool>	_finished; // Dfs(i)가 종료 되었는지 여부
	vector<int32>	_parent;	//발견된 부모노드
};

 

cpp

#include "pch.h"
#include "DeadLockProfiler.h"

/*--------------------
	DeadLockProfiler
---------------------*/

void DeadLockProfiler::PushLock(const char* name)
{
	LockGuard guard(_lock);

	// 아이디를 찾거나 발급한다.
	int32 lockId = 0;

	auto findIt = _nameToId.find(name);
	if (findIt == _nameToId.end())
	{
		lockId = static_cast<int32>(_nameToId.size());
		_nameToId[name] = lockId;
		_idToName[lockId] = name;
	}
	else
	{
		lockId = findIt->second;
	}

	// 잡고 있는 락이 있었다면
	if (_lockStack.empty() == false)
	{
		// 기존에 발견되지 않은 케이스라면 데드락 여부 다시 확인한다.
		const int32 prevId = _lockStack.top();
		if (lockId != prevId)
		{
			set<int32>& history = _lockHistory[prevId];
			if (history.find(lockId) == history.end())
			{
				history.insert(lockId);
				CheckCycle();
			}
		}
	}

	_lockStack.push(lockId);
}

void DeadLockProfiler::PopLock(const char* name)
{
	LockGuard guard(_lock);

	if (_lockStack.empty())
		CRASH("MULTIPLE_UNLOCK");

	int32 lockId = _nameToId[name];
	if (_lockStack.top() != lockId)
		CRASH("INVALID_UNLOCK");

	_lockStack.pop();
}

void DeadLockProfiler::CheckCycle()
{
	const int32 lockCount = static_cast<int32>(_nameToId.size());
	_discoveredOrder = vector<int32>(lockCount, -1);
	_discoveredCount = 0;
	_finished = vector<bool>(lockCount, false);
	_parent = vector<int32>(lockCount, -1);

	for (int32 lockId = 0; lockId < lockCount; lockId++)
		Dfs(lockId);

	// 연산이 끝났으면 정리한다.
	_discoveredOrder.clear();
	_finished.clear();
	_parent.clear();
}

void DeadLockProfiler::Dfs(int32 here)
{
	if (_discoveredOrder[here] != -1)
		return;

	_discoveredOrder[here] = _discoveredCount++;

	// 모든 인접한 정점을 순회한다.
	auto findIt = _lockHistory.find(here);
	if (findIt == _lockHistory.end())
	{
		_finished[here] = true;
		return;
	}

	set<int32>& nextSet = findIt->second;
	for (int32 there : nextSet)
	{
		// 아직 방문한 적이 없다면 방문한다.
		if (_discoveredOrder[there] == -1)
		{
			_parent[there] = here;
			Dfs(there);
			continue;
		}

		// here가 there보다 먼저 발견되었다면, there는 here의 후손이다. (순방향 간선)
		if (_discoveredOrder[here] < _discoveredOrder[there])
			continue;

		// 순방향이 아니고, Dfs(there)가 아직 종료하지 않았다면, there는 here의 선조이다. (역방향 간선)
		if (_finished[there] == false) //이곳에 들어오면 무조건 크래쉬가 남
		{
			printf("%s -> %s\n", _idToName[here], _idToName[there]);

			int32 now = here;
			while (true)
			{
				printf("%s -> %s\n", _idToName[_parent[now]], _idToName[now]);
				now = _parent[now];
				if (now == there)
					break;
			}

			CRASH("DEADLOCK_DETECTED");
		}
	}

	_finished[here] = true;
}

 

📌 DeadLockProfiler의 흐름

1. 락 사용 추적

void PushLock(const char* name);
void PopLock(const char* name);

 

  • 어떤 락을 잡는지(PushLock)와 해제하는지(PopLock)를 추적
  • 락 이름을 문자열로 받고, 내부적으로 고유한 ID를 할당해 사용

2. 락 간 순서 추적

map<int32, set<int32>> _lockHistory;
  • 락을 잡는 순서를 그래프로 저장
    예: LockA → LockB 라면 A → B 간선 생성

3. 사이클 탐지

void CheckCycle();

 

 

  • 락 사용 그래프에서 사이클이 생기면 데드락 가능성이 있다는 뜻
  • DFS 기반의 그래프 탐색으로 사이클을 탐지
  • 사이클이 발견되면 CRASH("DEADLOCK_DETECTED") 로 강제 종료
/*---------------
	  Crash
---------------*/

#define CRASH(cause)						\
{											\
	uint32* crash = nullptr;				\
	__analysis_assume(crash != nullptr);	\
	*crash = 0xDEADBEEF;					\
}

 

 

 

사이클 탐지 알고리즘

사이클 탐지는 방향 그래프에서 순환(cycle)을 찾는 것과 동일

DFS 중 핵심 로직

if (_finished[there] == false) 
{
    // here -> there 로 이어지는 역방향 간선 = 사이클 존재!
    CRASH("DEADLOCK_DETECTED");
}

 

 

AccountManager

#pragma once

class AccountManager
{
	USE_LOCK;

public:
	void AccountThenPlayer();
	void Lock();
};

extern AccountManager GAccountManager;

//-----------cpp
AccountManager GAccountManager;

void AccountManager::AccountThenPlayer() 
{
	WRITE_LOCK;
	GPlayerManager.Lock();
}

void AccountManager::Lock()
{
	WRITE_LOCK;
}

 

PlayerManager

#pragma once

class PlayerManager
{
	USE_LOCK;

public:
	void PlayerThenAccount();
	void Lock();
};

extern PlayerManager GPlayerManager;
PlayerManager GPlayerManager;

void PlayerManager::PlayerThenAccount()
{
	WRITE_LOCK;
	GAccountManager.Lock();
}

void PlayerManager::Lock()
{
	WRITE_LOCK;
}

Main 

#include "PlayerManager.h"
#include "AccountManager.h"

int main()
{
	GThreadManager->Launch([=]
		{
			while (true)
			{
				cout << "PlayerThenAccount" << endl;
				GPlayerManager.PlayerThenAccount();
				this_thread::sleep_for(100ms);
			}
		});

	GThreadManager->Launch([=]
		{
			while (true)
			{
				cout << "AccountThenPlayer" << endl;
				GAccountManager.AccountThenPlayer();
				this_thread::sleep_for(100ms);
			}
		});

	GThreadManager->Join();
}

 

결과 화면

728x90
반응형

'서버 > 멀티쓰레드 프로그래밍' 카테고리의 다른 글

Reader-Write Lock  (0) 2025.06.04
Lock Free Stack  (0) 2025.06.02
LockQueue LockStack  (0) 2025.06.02
TLS (Thread Local Storage)  (0) 2025.06.02
메모리 모델  (1) 2025.05.30