서버/멀티스레드

5. SpinLock

광란의슈가슈가룬 2024. 7. 14. 05:02

SpinLock이란 스레드가 공유자원을 활용하기 위해 무한루프를 빙빙돌며(Spin) 자원의 상태를 확인하는 것이다.

자원이 unlock 상태가 될 때까지 그냥 무작정 기다리는 것이다.

#include <iostream>
#include <thread>
#include <mutex>

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

		_locked = true;
	}

	void unlock()
	{
		_locked = false;
	}

private:
	bool _locked = false;
};

int sum = 0;
SpinLock spinLock;

void Add()
{
	for (int i = 0; i < 100000; i++)
	{
		lock_guard<SpinLock> guard(spinLock);
		sum++;
	}
}

void Sub()
{
	for (int i = 0; i < 100000; i++)
	{
		lock_guard<SpinLock> guard(spinLock);
		sum--;
	}
}

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

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

	cout << sum << endl;
}

10만 번 더하고 빼는 스레드들을 스핀락을 이용해 간단히 구현해 보았다.

빌드를 했을 땐 아무 문제가 없지만 실행을 시켜보니 원하는 결과값이 나오지 않았다.

출력 결과1
출력 결과2

위와 같이 실행할 때마다 결과가 달라진다.

이러한 결과를 보고 스레드 간의 컨텍스트 스위칭이 일어나면서 공유자원에 동시에 접근하게 된 것이 아닐까 생각을 해보았다.

 

lock() 어셈블리어

어셈블리어를 확인해 보면 단순히 1줄의 코드가 아니다.

여러개의 스레드가 _locked가 false일 때 동시에(정확히 동시는 아니지만) while문을 빠져나와 _locked에 접근하게 되면서 생기는 문제이다.

 

결국은 공유자원의 상태를 확인하고 lock을 획득하는 것이 동시에 일어나야 한다. 즉, Atomicity(원자성)을 가져야 되는 것이다.

 

atomic<bool> _locked = false;

그렇다면 위의 코드에서 _locked에 atomic키워드를 사용하면 _locked 변수은 원자성을 가지게 되니 괜찮지 않을까?

당연하게도 아직 Lock 상태를 검사하는 while문과 Lock을 걸게 되는 _locked = true가 나뉘어 있으니 아직 부족하다.

 

그렇다면 어떻게 해야 이를 해결할 수 있을까?

CAS(Compare and Swap)이라는 기법이 있다.

그렇다면 이게 무슨 기법이냐? 말 그대로 비교(Compare) 후 교환(Swap)하는 것이다.

특정 변수에 저장되어 있을 것이라 예상한 값과 같으면 교환하고 그렇지 않으면 버리는 것이다.

----------------------------------------------------------------------------------------------------------------

a라는 변수에 10이 들어있을 것이라 예상하고 이를 30으로 바꾸려고 한다고 해보자.

그런데 이 때 다른 스레드가 이미 a라는 값을 20이라고 바꾸게 되면

10이 있을 것이라 예상했지만 20이 있으니 30으로 바꾸라는 요청은 실패할 것이다.

반대로 10이 그대로 있으면 30으로 바꾸게 될 것이다.

----------------------------------------------------------------------------------------------------------------

compare_exchange_strong() 함수

atomic 클래스에 compare_exchange_strong이라는 CAS 기능을 해주는 함수가 있다.

인자로 Expected와 Desired를 받는데 Expected는 비교할 값, Desired는 바꿀 값이다.

이 함수를 Atomicity(원자성)을 가지며 값을 바꾸는데 성공하면 true, 실패하면 false를 리턴한다.

class SpinLock
{
public:
	void lock()
	{
		//CAS
		bool expected = false;
		bool desired = true;

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

	void unlock()
	{
		_locked = false;
	}

private:
	atomic<bool> _locked = false;
};

SpinLock 클래스의 코드를 위와 같이 수정하게 되면 코드가 의도한 대로 작동하는 것을 볼 수 있다.

while문 안에서 expected 변수를 초기화 해주는 이유는 compare_exchange_strong함수가 false일 때 expected에 _locked의 값을 넣기 때문이다.

CAS 출력 결과

하지만 위와 같이 무한정 대기를 하는 것이 과연 효율적일까?

Lock을 점유하고 있는 다른 스레드가 금방 Lock을 놓아줄 것이라 예상되면 기다리는 것이 효율적일 수도 있다.

반면, Lock을 굉장히 오랜시간 동안 점유하고 있는 경우에는 계속해서 compare_exchange_strong함수를 통해 체크를 하게 되고 이는 CPU의 불필요한 낭비로 이어지게 된다.

'서버 > 멀티스레드' 카테고리의 다른 글

7. Event  (0) 2024.07.20
6. Sleep  (0) 2024.07.17
4. DeadLock  (0) 2024.07.11
3. Lock  (0) 2024.07.11
2. Atomic  (0) 2024.07.10