dh_0e

[C++/Game Server] Spin Lock, Volatile 변수 본문

C++/Game Server

[C++/Game Server] Spin Lock, Volatile 변수

dh_0e 2025. 7. 26. 10:37

Spin Lock

  • Lock 메커니즘 중 하나로, 스레드가 lock을 얻을 때까지 계속해서 루프를 돌며 기다리는(spin) 방식
  • 멀티스레드 환경을 제대로 이해하고있는지 확인하기 위해 면접에서 주로 묻는 개념
class SpinLock
{
public:
	void lock() {
		while (_locked) {

		}
		_locked = true;
	}

	void unlock() {
		_locked = false;
	}
private:
	volatile bool _locked = false;
};
  • 위 코드의 lock() 함수를 보면 _locked가 false가 될 때까지 while 문으로 spin하다가 lock이 풀리면 즉시 lock을 점유하는 Spin Lock의 방식을 확인할 수 있음
  • 하지만 위 코드로 멀티스레드 환경을 실행해보면 스레드가 겹쳐서 결과값이 제대로 안 나옴
    • lock이 풀려있는지 확인하는 while문과, lock을 획득하는 제어문이 분리되어 있어서 while문을 동시에 통과한 두 스레드 t1, t2가 있을 시에 동시에 lock을 걸어주면서 문제가 발생

이를 해결하기 위해 atomic으로 묶어주는 함수를 CAS(Compare-And-Swap) 계열 함수라고 함

 

CAS(Compare-And-Swap) 함수

//cAS 의사(pseduo) 코드
if (_locked == expected)
{
	_locked = desired;
	return true;
}
else {
	expected = _locked;
	return false;
}
  • 동기화 원자 연산(atomic operation) 중 하나로, 위에서 발생한 문제를 해결하기 위해 lock이 풀려있는지 확인과 lock 획득을 동시에 실행할 수 있게끔 해주는 함수로 atomic에 포함되어 있음

class SpinLock
{
public:
	void lock() {
		bool expected = false;
        	bool desired = true;
        
		while (_locked.compare_exchange_strong(expected, desired) == false) {
			expected = false;
		}
	}

	void unlock() {
		_locked.store(false);
	}
private:
	atomic<bool> _locked = false;
};
  • compare_exchange_strong() 함수는 CAS 수도 코드처럼 _locked가 expected(false)이면 _locked를 획득하는 로직을 동시에 가지고 있음
  • std::mutex도 더 복잡하긴 하지만 CAS 기반 동기화 로직을 포함하고 있음

 

Volatile 변수

  • 맨 위 코드에 _locked를 선언할 때 앞에 붙은 키워드로, C/C++에서 컴파일러에게 해당 변수는 언제든 외부에서 변경될 수 있으니, 최적화를 하지 말고 항상 메모리에서 읽고 쓰도록 명령하는 키워드
int a = 1;
a = 2;
a = 3;
a = 4;
  • 위 예시 코드를 컴파일한 어셈블리어로 바꾸는 과정에선 컴파일러가 최적화를 해서 앞 과정을 생략하고 a를 4라는 값으로만 기억함
volatile int a = 1;
a = 2;
a = 3;
a = 4;
  • 키워드에 volatile을 추가하면 a=1, a=2, a=3 모두 쓸데없지만 최적화하지 않고 모두 메모리에 값을 저장하는 작업을 수행하게 됨
volatile bool running = true;

void thread1() {
    while (running) {
        // do something
    }
}

void thread2() {
    running = false;
}
  • 다음과 같은 예시에서도 running 변수를 volatile로 선언하지 않는다면, thread1이 running을 레지스터에 저장해놓고 바뀌지 않는다고 착각할 수 있음
    • 이를 토대로 최적화한다면 running의 값을 사용할 때마다 메모리에서 직접 읽어오지 않아, thread2가 running 값을 바꿔도 thread1은 감지하지 못함
  • 하지만 volatile 변수로 선언해줌으로써 running을 항상 메모리에서 읽어오며 thread2로 값이 변경되는 것을 감지할 수 있음