[Server] #15-7. 스핀락

스핀락

이번에는 Lock 클래스를 직접 구현하여 멀티 스레드 환경에서의 동기화 문제를 확인하고, CAS를 사용하여 락을 원자적으로 구현하는 방법을 익혀본다. 그리고 CAS를 이용해 스핀락을 구현하는 방법을 배우고, 스핀락의 장단점을 분석해본다.


실습1 - Lock 클래스 구현하기

Lock 클래스를 직접 구현하여 동기화 문제를 확인해보자.

다음 코드의 결과를 예측해보자.

  1. 20000
  2. Crash 발생
  3. 이상한 값
class Lock
{
public:
	void lock()
	{
		while (_flag)
		{
			// Do Nothing
		}

		_flag = true;
	}

	void unlock()
	{
		_flag = false;
	}

private:
	bool _flag = false;
};

Lock m;
std::vector<int> v;

void Push()
{
	for (int i = 0; i < 10000; i++)
	{
		std::lock_guard<Lock> lockGuard(m);

		v.push_back(i);
	}
}

int main()
{
    v.reserve(100000);

    thread t1(Push);
    thread t2(Push);
    thread t3(Push);

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

    std::cout << v.size() << endl;
}

 

  • 결과
    1. Crash가 발생한다.
    2. 이상한 값이 출력된다. (vector.reserve(1000000)을 사용해 이사 횟수를 줄였을 때)
  • 원인
    • while (_flag)_flag = true가 두 단계로 분리되어 실행되기 때문이다.
    • 스레드 t1과 t2가 동시에 lock()을 호출했을 때, t1이 While (_flag)를 통과하고, _flag = true;를 실행하기 전에 t2가 while (_flag)를 통과할 수 있다.
    • 즉, 상호 배타적인 조건이 깨지면서 동시에 vector에 접근하는 문제가 발생한다.
  • 결론
    • 기본 Lock 클래스는 상호 배타성을 보장하지 못해 스레드 간 동기화가 깨질 수 있다.
image image

실습2 - CAS 적용하기

💡 CAS(Compare-And-Swap)란?

CAS는 원자적 연산(Atomic Operation) 중 하나로, 현재 변수 값과 기대 값(예상 값)을 비교하여, 기대 값과 일치하면 새로운 값으로 변경하는 방식이다.

  • CAS 동작 과정
    1. 현재 값(Current Value)을 읽어온다.
    2. 기대 값(Expected Value)과 비교한다.
      • 만약 (현재 값 == 기대 값)이면, 새로운 값으로 업데이트한다.
      • 만약 (현재 값 != 기대 값)이면, 업데이트 하지 않고 다시 시도한다.

즉, 다른 스레드가 값을 변경하지 않았다면 그대로 변경하고, 다른 스레드가 값을 변경했다면 다시 확인하는 방식이다.

 

  • 실습1의 문제를 해결하려면, _flag 값을 원자적으로 변경해야 한다. 이를 위해 CAS(Compare-And-Swap)를 이용해 두 단계를 하나로 합쳐보자.
class Lock
{
public:
	void lock()
	{
		// CAS (Compare-And-Swap)
		// 두 단계를 하나로 합친 코드
		bool expected = false;
		bool desired = true;

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

	void unlock()
	{
		_flag = false;
	}

private:
	std::atomic<bool> _flag = false;

 

  • 의사 코드
    • 이 과정을 원자적으로 실행해 여러 스레드가 동시에 접근해도 문제가 없다.
if (_flag == expected)   // 현재 값이 예상 값과 같은지 확인
{
    _flag = desired;    // 예상 값과 같다면 새로운 값으로 변경
    return true; 
}
else
{
    expected = _flag;   // 예상 값이 다르면 현재 값을 다시 읽어옴
    return false;
}

 

  • CAS 적용 후 변화
    • compare_exchange_strong()을 사용하면 _flag가 기대 값(false)일 때만 true로 변경한다.
    • 단 한 번의 연산으로 _flag 값을 변경하여 스레드 간 동기화를 보장한다.
    • 즉, 스레드 t1이 _flagtrue로 변경하는 동안 다른 스레드는 _flag 변경이 끝날 때까지 대기해야 한다.
  • 기존 방식과 비교하기
      while (_flag) {}   // 기존 코드 - 단계 1: 현재 _flag 값 검사
      _flag = true;      // 기존 코드 - 단계 2: _flag 값을 변경
    
    • while (_flag)를 통과한 후 _flag = true;로 변경하는 사이에 다른 스레드가 들어올 수 있다.
    • CAS를 사용하면 이 두 단계를 하나의 연산으로 처리하여 동기화 문제를 해결할 수 있다.
  • 결론
    • CAS를 사용하면 락 획득 과정을 원자적으로 처리하여 동기화 문제를 해결할 수 있다.

실습3 - 스핀락

  • 스핀락은 락이 해제되었는지 계속 루프를 돌며 확인하고, 해제되면 즉시 락을 획득하는 방식이다.
  • CAS(Compare-And-Swap) 기법을 사용하며 락을 원자적으로 획득한다.
  • 예. 화장실 앞에서 앞 사람이 나오길 기다리는 것과 같다.
  • 스레드가 락을 획득할 때까지 게속 CPU를 사용하며 대기하므로 바쁜 대기(busy-wating) 방식이라고도 부른다.
while (_flag.compare_exchange_strong(OUT expected, desired) == false)
{
	expected = false;
}

 

  • 스핀락의 장단점
    • 장점
      • 아주 짧은 순간만 lock을 잡을 경우 빠르다.
      • lock을 오래 잡지 않는다면 효율적이다. (예. MMO 서버에서 자주 사용)
    • 단점
      • 계속 CPU를 사용하기 때문에 비용이 크다.
      • CPU를 오래 붙잡으면 다른 스레드가 계속 대기해야 한다.
      • 대기 시간이 길어지면 비효율적이다.
  • 결론
    • 스핀락은 주로 아주 짧은 락 구간에 유용하고, 긴 시간 락을 잡아야 할 때는 일반적인 Mutex를 사용하는 게 더 효율적이다.

Mutex vs CAS

  • 화장실 비유
    • 화장실이 1개 있고 여러 명이 사용하려고 대기 중이다.
    • 누군가 화장실에 들어가면 ‘사용중’ 표시가 되고, 들어가려는 사람은 화장실이 비었으면 즉시 들어간다.
    • 여기서 여러 명이 동시에 들어가려고 하면 문제가 발생할 수 있다.
  • Mutex Lock
    • 사람들이 화장실 문 앞에 줄을 서서 차례대로 들어간다.
    • 문 앞에 관리자(운영체제)가 한 명씩 들여보낸다.
    • 문이 잠겨 있으면, 관리자는 대기 중인 사람을 대기실에서 쉬게한다.
    • 화장실을 사용한 사람이 나오면, 관리자가 가장 먼저 기다리던 사람을 들여보내 준다.
    • 장단점
      • 장점: 기다리는 동안 계속 문 앞에 서 있지 않아도 된다. (CPU 낭비가 없다.)
      • 단점: 기다리는 동안 CPU는 다른 일을 할 수 없다. (대기 상태), 누가 먼저 들어갈지 결정(OS 스케줄링)해야 하는 비용이 든다.
  • CAS
    • 사람들이 줄을 서지 않고, 각자 문이 열려 있는지 확인한다.
    • 화장실이 ‘사용중’이면, 다시 문이 열릴 때까지 시도한다.
    • 문이 열려 있으면, 빨리 들어가서 ‘사용 중’을 표시한다.
    • 문을 연 사람 한 명만 성공하고, 나머지는 다시 시도하게 만든다.
    • 장단점
      • 장점: 락이 짧게 걸릴 때 빠르다.
      • 단점: 누군가 화장실을 오래 사용하면, 계속 확인해야 해서 CPU를 많이 쓴다. (바쁜 대기)

출처 Rookiss님 게임 프로그래머 입문 올인원

Categories:

Updated:

Leave a comment