KoreanFoodie's Study

[C++ 게임 서버] 1-7. SpinLock 개념과 구현 본문

Game Dev/Game Server

[C++ 게임 서버] 1-7. SpinLock 개념과 구현

GoldGiver 2023. 7. 11. 23:16

Rookiss 님의 '[C++과 언리얼로 만드는 MMORPG 게임 개발 시리즈] Part4: 게임 서버' 를 들으며 배운 내용을 정리하고 있습니다. 관심이 있으신 분은 꼭 한 번 들어보시기를 추천합니다!

[C++ 게임 서버] 1-7. SpinLock

핵심 :

1. SpinLock 은 Lock 을 잡기 위해 '무작정 기다리는' 방식이다.

2. SpinLock 은 atomic 변수와 lock, unlock 함수로 쉽게 만들 수 있다. compare_exchange_strong 함수를 기억하자.

3. SpinLock 은 불필요한 CPU clock 을 낭비하지만, 때로는 컨텍스트 스위칭에 드는 것보다 비용이 저렴할 수도 있다.

이제 본격적으로 SpinLock 을 구현해 보자.

SpinLock 은 간단하다. 그냥 Lock 을 잡을 때까지 무작정 While 문을 돌면서 기다리도록 만들 것이다 🤣

간단하게 생각하면 아래와 같이 구현하겠지만...

int sum = 0;

class SpinLock
{
private:
	bool _lock = false;

public:
	void lock()
	{
		while (_lock)
		{

		}

		_lock = true;
	}

	void unlock()
	{
		_lock = false;
	}
};

SpinLock spinLock;

void Add()
{
	for (int i = 0; i < 1000; ++i)
	{
		spinLock.lock();

		++sum;
		
		spinLock.unlock();
	}
}

void Sub()
{
	for (int i = 0; i < 1000; ++i)
	{
		spinLock.lock();

		--sum;

		spinLock.unlock();
	}
}

int main()
{
	thread t1(Add);
	thread t2(Sub);

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

	cout << "sum : " << sum << endl;
}

위 코드는 제대로 동작하지 않는다.

돌릴 때마다 sum 값이 달라진다

그 이유는 크게 2 가지가 있다.

 

일단, SpinLock 클래스의 _locked 는 멀티쓰레드 환경을 고려하지 않고 선언한 녀석이다. 

무슨 말인고 하니, 다음과 같은 상황이 있다고 가정해 보자.

int a = 0;
a = 1;
a = 2;
a = 3;

컴파일러는 이 코드를 어셈블리어로 만들 때, 매우 '똑똑한' 최적화를 한다. 즉, a 의 최종값은 어차피 3이니, 굳이 0, 1, 2 값을 대입하지 않고 바로 3 을 넣는 어셈블리어만 생성하는 것이다.

'모종의' 이유로, 컴파일러가 이러한 최적화를 하지 않도록 하려면, 변수를 선언할 때 volatile 을 붙여주면 된다!

// 이제 대입할 때마다 그에 맞는 어셈블리어가 생성된다.
volatile int a = 0;

아, 물론 atomic 을 사용해도 된다. atomic 은 volatile 을 포함하는 녀석이다 😉

 

이제  SpinLock 을 아래와 같이 제대로 구현할 것이다... LockGuard 도 사용해서 😂

참고로, 우리가 만드는 클래스에 lock 과 unlock 함수가 있으면 std::lock_guard 를 만들때 넣어서 사용할 수 있다!

class SpinLock
{
private:
	atomic<bool> _locked = false;

public:
	void lock()
	{
		bool expected = false;
		bool desired = true;

		while (_locked.compare_exchange_strong(expected, desired) == false)
		{
			expected = false;
		}
	}

	void unlock()
	{
		_locked.store(false);
	}
};

SpinLock spinLock;

void Add()
{
	for (int i = 0; i < 10'000; ++i)
	{
		lock_guard<SpinLock> lockGuard(spinLock);
		++sum;
	}
}

위 코드를 보면, compare_exchange_strong 이라는 녀석이 나오는데, 아래 부분의 의미는 다음과 같다 :

if (_locked == expected)
{
    expected = _locked;
    _locked = desired;
    return true;
}
else
{
    expected = _locked;
    return false;
}

핵심은 _locked 가 expected 와 같은지를 판단하는 것이다. 그런데 expected 는 항상 _locked 의 값을 가지게 되는데...

설명을 잘 보면 expected 변수를 참조하여 사용하고 있다. 따라서, Lock 을 실제로 잡는데 성공했을 때에는, expected 값을 다시 false 로 바꾸어 주어야 한다! 😊

Comments