[Server Programming/cpp] SpinLock

앞에서 Lock의 기본적인 사용에 대해 알아 보았다. 이번에는 SpinLock을 직접 구현해보고 특성을 파악해보려 한다.

SpinLock

void Add() {
	for (int32 i = 0; i < 10'000; i++)
	{
		lock_guard<mutex> guard(_mutex);
		sum++;
	}
}

void Sub() {
	for (int32 i = 0; i < 10'000; i++)
	{
		lock_guard<mutex> guard(_mutex);
		sum--;
	}
}

위와 같이 1만번을 더하고 빼는 두 함수를 다중 쓰레드로 실행하려 한다. 현재 mutex를 통해서 락을 하고있고, 실행해보면 정상적인 결괏값을 얻을 수 있다.

SpinLock으로 구현 1

class SpinLock {
public:
	void lock() {
		while (_locked) {

		}
		_locked = true;
	}
	void unlock() {
		_locked = false;
	}
private:
	bool _locked = false;
};
...

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

으로 락을 직접 생성한 SpinLock 클래스로 대체 해보았다. 그 결과는 어땠을까?

-139 ... // 생각한대로 나오지 않는다. 어디가 문제일까?

volatile

빌드 구성에 Debug와 Release모드가 있는데, 이 둘의 차이는 컴파일 시 컴파일러가 최적화를 해주냐 안해주냐에 있다.

그리고 volatile 접두사는 한 변수를 컴파일 시 어떤 모드에서든 최적화하지 말라는 의미가 된다 (c++에서)

예제를 보면 이해가 쉽다.

최적화가 되지 않은 경우, Debug모드일때

00007FF73DC0747D  nop  
	int32 a;
	a = 0;
00007FF73DC0747E  mov         dword ptr [a],0  
	a = 1;
00007FF73DC07485  mov         dword ptr [a],1  
	a = 2;
00007FF73DC0748C  mov         dword ptr [a],2  
	a = 3;
00007FF73DC07493  mov         dword ptr [a],3  
	a = 4;
00007FF73DC0749A  mov         dword ptr [a],4  
	a = 5;
00007FF73DC074A1  mov         dword ptr [a],5  
	cout << a << endl;

모든 코드에 대해 한줄 한줄 어셈블리어로 변환되어 실행된다.

이는 volatile int32이라고 선언했을때와 같은 효과를 갖는다.

컴파일러에 의해 최적화가 된 경우

	int32 a;
	a = 0;
	a = 1;
	a = 2;
	a = 3;
	a = 4;
	a = 5;
	cout << a << endl;
00007FF6E3731077  mov         edx,5  

Release모드로 빌드한다면 어셈블리어로 위와 같은 결과를 볼 수 있다.

어차피 a=5로 덮어지기에 전부 무시하고 a=5만 어셈블리어로 실행되는 모습.

여기서 volatile을 int32 a에 적용하면 첫번째 예시처럼 실행될 것이다.

volatile을 설명한 이유

위의 lock 예제에서 루프문에서 또한 최적화가 일어나면 검사를 일일히 안하기 때문이다.

왜 그럴까? cpu는 다른 스레드의 존재를 인식하지 못하기 때문에 이미 변수가 false인 상황에서 반복문의 조건을 따질 이유가 없기 때문이다.

void lock() {
	while (_locked) {
		// 어차피 _locked가 false이니 검사 계속 안할래~
	}
	_locked = true;
}

하지만 멀티스레드 환경에서는 다른 스레드에 의해 변수가 언제든지 변할 수 있기에 우리는 무한 루프를 돌렸던 것이다.

따라서 volatile을 할당하면 컴파일러의 의사와 상관없이 최적화 시키지말고 있는 그대로 해석해달라는 의미가 된다.

volatile bool _locked = false;

atomic을 사용하자

하지만 실전에선

volatile bool _locked = false; 

가 아닌 atomic으로 locked를 감싼다.

atomic<bool> locked = false;

atomic에서 volatile 기능까지 포함하기 때문.

출처

인프런 Rookiss, "[C++과 언리얼로 만드는 MMORPG 게임 개발 시리즈] Part4: 게임 서버"