spin lock 무한 루프를 돌면서 lock 이 풀릴때까지 계속 대기 하는것(Busy wait)

context swith 가 자주 되지 않는 다는 장점이 있지만 오랬 동안 걸릴것 같다면 그 사이 아무것도 못함으로 그 기다리는 동안 부하가 생기게 된다

바쁘게 기다린다는 것은 무한 루프를 돌면서 최대한 다른 스레드에게 CPU 양보하지 않는 것이다.

 

 

Spin Lock  이름이 뜻하는대로만약 다른 스레드가 lock 소유하고 있다면  lock 반환될 때까지 계속 확인하며 기다리는 것이다. "조금만 기다리면 바로   있는데 굳이 컨텍스트 스위칭으로 부하를  필요가 있나?" 라는 컨셉으로 개발된 것으로 크리티컬 섹션에 진입이 불가능할때 컨텍스트 스위칭을 하지 않고 잠시 루프를 돌면서 재시도 하는 것을 말합니다. Lock-Unlcok 과정이 아주 짧아서 락하는 경우가 드문 경우(적절하게 크리티컬 섹션을 사용한 경우유용하다Spin Lock  다음과 같은 특성을 갖는다.

  1. Lock 얻을  없다면계속해서 Lock 확인하며 얻을 때까지 기다린다이른바 바쁘게 기다리는 busy wating이다.
  2. 바쁘게 기다린다는 것은 무한 루프를 돌면서 최대한 다른 스레드에게 CPU 양보하지 않는 것이다.
  3. Lock  사용가능해질 경우 컨택스트 스위치를 줄여 CPU 부담을 덜어준다하지만만약 어떤 스레드가 Lock 오랫동안 유지한다면 오히려 CPU 시간을 많이 소모할 가능성이 있다.
  4.  하나의 CPU 하나의 코어만 있는 경우에는 유용하지 않다 이유는 만약 다른 스레드가 Lock 가지고 있고  스레드가 Lock 풀어 주려면 싱글 CPU 시스템에서는 어차피 컨택스트 스위치가 일어나야 하기 때문이다. 주의할  스핀락을 잘못 사용하면 CPU 사용률 100% 만드는 상황이 발생하므로 주의 해야 한다스핀락은 기본적으로 무한 for 루프를 돌면서 lock 기다리므로 하나의 쓰레드가 lock 오랫동안 가지고 있다면다른 blocking 쓰레드는 busy waiting 하므로 CPU 쓸데없이 낭비하게 된다.

장점은 스핀락을  사용하면 context switch 줄여 효율을 높일  있습니다. 무한 루프를 돌기 보다는 일정 시간 lock 얻을  없다면 잠시 sleep하는 back off 알고리즘을 사용하는 것이 훨씬 좋습니다.

 

 

 

 

스핀 락(Spin lock)과 뮤텍스(Mutex)의 차이

둘 모두 자원에 대해 락을 걸고 사용하려고 할 시에 락이 풀릴 때까지 기다려야 한다는 점은 같지만, 둘은 내부적으로 로우레벨에서 차이점이 있다.

 

 우선 뮤텍스의 경우, 자원에 이미 락이 걸려 있을 경우 락이 풀릴 때까지 기다리며 컨텍스트 스위칭을 실행한다. 

즉, 다른 병렬적인 태스크를 처리하기 위해 CPU를 양보할 수 있다는 것이며 이는 자원을 얻기 위해 오랜 시간을 기다려야 할 것이 예상될 때 다른 작업을 동시에 진행할 수 있다는 것이다. 하지만 이는 자원이 단시간 내로 얻을 수 있게 될 경우 컨텍스트 스위칭에 더 큰 자원을 낭비하게 될 수 있다는 문제가 있다.

 스핀 락의 경우에는 이름에서부터 알 수 있듯이, 자원에 락이 걸려 있을 경우 이를 얻을 때까지 무한 루프를 돌면서 다른 태스크에 CPU를 양보하지 않는 것이다. 자원이 단시간 내로 얻을 수 있게 된다면 컨텍스트 스위칭 비용이 들지 않으므로 효율을 높일 수 있지만, 그 반대의 경우 다른 태스크에 CPU를 양보하지 않으므로 오히려 CPU 효율을 떨어뜨릴 수 있는 문제가 있다.

 

 

 

 

 

 

#include "pch.h"
#include <iostream>
#include "CorePch.h"
#include <thread>	
#include <atomic>			
#include <vector>
#include <mutex>
#include "AccountManager.h"
#include "UserManager.h"

using namespace std;

mutex m1;
int32 sum = 0;


void func1()
{
	for (int32 i=0;i<10000;++i)
	{
		lock_guard<mutex> guard(m1);
		sum++;
	}
}


void func2()
{
	for (int32 i = 0; i < 10000; ++i)
	{
		lock_guard<mutex> guard(m1);
		sum--;
	}
}

int main()
{

	thread t1(func1);
	thread t2(func2);

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

	std::cout << sum << std::endl;


	return 0;
}

 

 

위 코드 결과는 0 이며 mutex 를 활용한 것이다 정상적으로처리 된다

 

하지만 아래처럼 custom 한 lock 클래스를 구현하여 처리 할려고 보면 아래와 같이 정사적인 연산이 되지 않는다는 것을 알 수 있다

 

#include "pch.h"
#include <iostream>
#include "CorePch.h"
#include <thread>	
#include <atomic>			//멀티 플랫폼에서 작동 가능
#include <vector>
#include <mutex>
#include "AccountManager.h"
#include "UserManager.h"

using namespace std;

mutex m1;
int32 sum = 0;

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

	void unlock()
	{
		_locked = false;
	}
	bool _locked = false;

};


SpinLock spinLock;


void func1()
{
	for (int32 i=0;i<10000;++i)
	{
		lock_guard<SpinLock> guard(spinLock);
		sum++;
	}
}


void func2()
{
	for (int32 i = 0; i < 10000; ++i)
	{
		lock_guard<SpinLock> guard(spinLock);
		sum--;
	}
}

int main()
{

	thread t1(func1);
	thread t2(func2);

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

	std::cout << sum << std::endl;


	return 0;
}

 

결과 -5512  등등 이상한 값이 나옴..

 

 

volatile 는 해당 코드를 최적화 하지 말라고 하는것 반복문 등에서.. 하지만 이게 지금의문제는 아니지만 참고사항으로

 

_locked = true; 이 코드는 한줄이 아님

_locked = true;
00007FF60EC7405F  mov         rax,qword ptr [this]  
00007FF60EC74066  mov         byte ptr [rax],1

bool 값도 원자적으로 일어나야지 문제가 발생하지 않음으로 atomic 으로 처리해야한다

또한 lock_guard 에서 lock 함수와 unlock 함수를 사용하는 이유에서라도 atomic 을 사용해야한다

 

 

 

문제는 아래 명령이 하나로 처리 되야 하는데 로직이 두개가 보니 즉 wait 하다가 값이 바뀌면 바로 빠져나가야 하는데 스레드가 전환 되면서 값이 제대로 바뀌지 않을 수 있기 때문에 문제가 발생하기 때문에

while (_locked)
{
}
_locked = true;

 

이게 한번의 명령으로 처리 되지 않아 context switch 가 되면서 상태가 망가지는 것

while 쪽의 조건과 _locked 값을 한번에 처리 하는 방식으로 대기하도록 바꿔줘야 할 필요가 있다

즉 한방에 처리해야 한다는 것

 

 

이것을 한방에 수행하도록 묶어주는 방법이 있음 

CAS (Compare-And-Swap)  : OS 에 따라서 InterlockedExchagned(..) 같은 함수인데

atomic 을 사용하면 여기에 이것이 포함되어 있음 

 

atomic.compare_exchange_strong 함수임

 

 

 compare_exchange_strong : 이 함수는 atomic 하게 일어나며 (원자적으로 한방에처리 all or nothing) 다음과 유사하다 

 

요약 하자면 expected 와 _locked 값이 같으면 desired 으로 _locked 값을 바꾸고, 그렇지 않다면 _locked 값은 그대로 유지된다
  

 

compare_exchange_strong  : 함수에 대한 설명 결국 값을 바꾸는 것이긴 한데 원하는 값으로 바꾸느냐임


if(_locked == expected )   //만약 _locked 가 기대되는 값이 false 와 같다면
{
    expected = _locked; //_lock 값을 기대되는 값에 넣어준다
    _locked = desired; //그 다음 _lock 값을 원하는 값으로 넣어준다
    return true; //즉 locked 가 기대하는 값과 같다면 _locked 를 원하는 값으로 변경한다
}
else    // _locked 가 기대하는 값과 다르다면
{
      expected = _locked; //기대 값을 _locked값으로 바꾼다
      return false;
}



 

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

		/*
		* compare_exchange_strong : 이 함수는 atomic 하게 일어나며
         (원자적으로 한방에처리 all or nothing) 다음과 유사하다 
		* 요약 하자면 expected 와 _locked 값이 같으면 desired 으로 _locked 값을 바꾸고, 
          그렇지 않다면 _locked 값은 그대로 유지된다
		* 
			if(_locked == expected )  	//만약 _locked 가 기대되는 값이 false 와 같다면
			{
				expected = _locked;		//_lock 값을 기대되는 값에 넣어준다
				_locked = desired;		//그 다음 _lock 값을 원하는 값으로 넣어준다
				return true;			//즉 locked 가 기대하는 값과 같다면 
                                        //_locked 를 원하는 값으로 변경한다
			}
			else	// _locked 가 기대하는 값과 다르다면
			{
				expected = _locked;		//기대 값을 _locked값으로 바꾼다
				return false;
			}
		*/
		
		//_locked 값이 기대 하는 값과 같으면 계속 무한 루프를 돌면서 spinlock 으로 계속 대기
        //하고, 그렇지 않고 expected 값이 locked 다르면 내부적으로 _locked 값을 desired 값으로
        //바꾼다음 while 을 탈출해 spinlock 에서 
        //빠져나오게 한다
        
		while (_locked.compare_exchange_strong(expected, desired) == false)
		{
        	//compare_exchange_strong 내부에서 expected 값을 & 로 바꾸기 때문에 
        	//원래 초기 상태로 바꾼다
			expected = false;			
            
		}
	}

	void unlock()
	{
		_locked.store(false);	//bool 을 바꾸는 것도 또한 원자적으로 일어나야함 
	}

 

 

 

 

 

Custom class 로 구혆나 Spinlock 클래스 

 

#include "pch.h"
#include <iostream>
#include "CorePch.h"
#include <thread>	
#include <atomic>			//멀티 플랫폼에서 작동 가능
#include <vector>
#include <mutex>
#include "AccountManager.h"
#include "UserManager.h"

using namespace std;

mutex m1;
int32 sum = 0;

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

		/*
		* compare_exchange_strong : 이 함수는 atomic 하게 일어나며(원자적으로 한방에처리 all or nothing) 다음과 유사하다 
		* 요약 하자면 expected 와 _locked 값이 같으면 desired 으로 _locked 값을 바꾸고 , 그렇지 않다면 _locked 값은 그대로 유지된다
		* 
			if(_locked == expected )  //만약 _locked 가 기대되는 값이 false 와 같다면
			{
				expected = _locked;	//_lock 값을 기대되는 값에 넣어준다
				_locked = desired;		//그 다음 _lock 값을 원하는 값으로 넣어준다
				return true;				//즉 locked 가 기대하는 값과 같다면 _locked 를 원하는 값으로 변경한다
			}
			else	// _locked 가 기대하는 값과 다르다면
			{
				expected = _locked;		//기대 값을 _locked값으로 바꾼다
				return false;
			}
		*/
		
		//_locked 값이 기대 하는 값과 같으면 계속 무한 루프를 돌면서 spinlock 으로 계속 대기 하고, 그렇지 않고 expected 값이 locked 다르면
		//내부적으로 _locked 값을 desired 값으로 바꾼다음 while 을 탈출해 spinlock 에서 빠져나오게 한다
		while (_locked.compare_exchange_strong(expected, desired) == false)
		{
			expected = false;		//compare_exchange_strong 내부에서 expected 값을 & 로 바꾸기 때문에 원래 초기 상태로 바꾼다
		}
		
	}

	void unlock()
	{
		_locked.store(false);	//bool 을 바꾸는 것도 또한 원자적으로 일어나야함 
	}
	volatile atomic<bool>  _locked = false;	//bool 값도 그냥 바꾸면 명령어가 두개로 처리됨

};


SpinLock spinLock;


void func1()
{
	for (int32 i=0;i<10000;++i)
	{
		lock_guard<SpinLock> guard(spinLock);
		sum++;
	}
}


void func2()
{
	for (int32 i = 0; i < 10000; ++i)
	{
		lock_guard<SpinLock> guard(spinLock);
		sum--;
	}
}

int main()
{

	thread t1(func1);
	thread t2(func2);

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

	std::cout << sum << std::endl;


	return 0;
}

 

 

spinlock 을 cpu 를 계속 먹는 상태가 된다 context switch 가 되지 않음으로

그래서 spinlock 끼리 서로 병합을 벌이게 되면 cpu 점유율이 확 올라가게 된다

SpinLock 은 커널레벨이 아닌 유저레벨에서 처리 되는 것임

context switch 가 되지 않아 점유율이 올라감으로 

 

 

 

 

 

반응형

+ Recent posts