스핀락(SpinLock)은 임계 구역(critical section)에 진입이 불가능할 때 진입이 가능할 때까지 루프를 돌면서 재시도하는 방식으로 구현된 락을 말한다.
스핀락이라는 이름은 락을 획득할 때까지 해당 스레드가 빙빙 돌고 있다(spinning)는 것을 의미한다.
스핀락은 두 개의 상태를 가질 수 있는 변수를 갖는 낮은 단계의 동기화 메커니즘이다. 두상태는 아래와 같다.
1. 획득됨 (acquired)
2. 해제됨 (released)
락의 장점은 '인터럽트 컨텍스트’에서도 사용할 수 있다는 것이다. 인터럽트 컨텍스트란, 컴퓨터가 중요한 작업을 처리하고 있을 때 다른 작업을 일시 중단하는 상황을 말한다. 이런 상황에서는 일반적으로 ‘잠자기’ 상태인 뮤텍스나 세마포어 같은 도구를 사용할 수 없다. 하지만 스핀락은 이런 상황에서도 사용할 수 있다.
그러나 스핀락의 단점은, 한 번 잠긴 스핀락은 다른 작업이 그것을 해제할 때까지 계속 잠긴 상태로 남아있다. 이는 컴퓨터가 스핀락이 해제되기를 기다리는 동안 다른 작업을 처리하지 못하게 만든다. 따라서 스핀락은 짧은 시간 동안만 사용해야 한다.
마지막으로, 스핀락을 사용하는 것이 좋은 경우는, 스핀락을 잠그고 푸는 시간이 다른 작업을 중단하고 다시 시작하는 시간보다 짧을 때이다. 이런 경우에는 스핀락을 사용하는 것이 더 효율적일 수 있다.
스핀락을 구현해 보도록 하겠다.
#include <iostream>
#include <thread>
#include <mutex>
int32 sum = 0;
mutex m;
void Add()
{
for (int32 i = 0; i < 10'0000; ++i)
{
lock_guard<mutex> guard(m);
sum++;
}
}
void Sub()
{
for (int32 i = 0; i < 10'0000; ++i)
{
lock_guard<mutex> guard(m);
sum--;
}
}
int main()
{
std::thread th_1(Add);
std::thread th_2(Sub);
th_1.join();
th_2.join();
cout << sum << endl;
return 0;
}
위 코드는 두개의 쓰레드가 서로 락을 잡아주면서 실행하기 때문에 출력은 0 이 뜬다.
SpinLock 을 구현하기 위해서는 atomic에서 주어지는 멤버함수인 compare_exchange_strong 함수를 사용할 것인데 Compare And Swap 개념의 함수이다.
`compare_exchange_strong`은 C++의 `std::atomic` 클래스에서 제공하는 함수이다. 이 함수는 원자적(atomic)으로 값을 비교하고 교환하는 작업을 수행한다.
`compare_exchange_strong` 함수는 아래와 같이 동작한다.
1. `std::atomic` 객체에 저장된 값과 `expected` 인자로 전달된 값이 같은지 비교한다.
2. 만약 두 값이 같다면, `std::atomic` 객체에 저장된 값을 `val` 인자로 전달된 값으로 교환한다.
3. 만약 두 값이 다르다면, `expected` 인자로 전달된 값을 `std::atomic` 객체에 저장된 값으로 교환한다.
이 함수는 항상 `std::atomic` 객체에 저장된 값을 읽어오며, 비교 결과가 참인 경우에만 값을 교환한다. 이런 특성 때문에 `compare_exchange_strong` 함수는 멀티스레딩 환경에서 안전하게 값을 비교하고 교환하는 데 사용된다. 아래는 CAS 의 의사코드이다. _locked 는 atomic<bool>변수이다.
bool expected = false;
bool desired = true;
//CAS 의사 코드
if (_locked == expected)
{
expected = _locked;
_locked = desired;
return true;
}
else
{
expected = _locked;
return false;
}
_locked.compare_exchange_strong(expected, desired);
이런식으로 말그대로 비교 후 스왑하는 개념이다. 그래서 compare_exchange_strong 함수를 사용하여 SpinLock을 구현해 보면
#include <iostream>
#include <thread>
#include <atomic>
#include <mutex>
class SpinLock
{
public:
void lock()
{
//CAS (Compare And Swap)
bool expected = false;
bool desired = true;
////CAS 의사 코드
//if (_locked == expected)
//{
// expected = _locked;
// _locked = desired;
// return true;
//}
//else
//{
// expected = _locked;
// return false;
//}
while (!_locked.compare_exchange_strong(expected, desired)) // 성공할때까지 무한정 시도 하겠다.
{
//첫번째 매개변수를 참조값을 받고 있기때문에 매번 우리가 원하는 값으로 세팅해 줘야함
expected = false;
}
}
void unlock()
{
//_locked = false;
_locked.store(false);//atomic 변수이기때문에 원자적으로 값을 저장하는 store 함수 사용
}
private:
//volatile bool _locked = false;//volatile : 컴파일러에게 최적화를 하지 말아달라는 요청 키워드, 멀티쓰레드 환경이기때문에 계속 변경될 수 있어 붙임
atomic<bool> _locked = false;//atomic이 volatile 의 개념까지 가지고있기때문에 atomic을 사용
};
int32 sum = 0;
mutex m;
SpinLock spinlock;
void Add()
{
for (int32 i = 0; i < 10'0000; ++i)
{
lock_guard<SpinLock> guard(spinlock);
sum++;
}
}
void Sub()
{
for (int32 i = 0; i < 10'0000; ++i)
{
lock_guard<SpinLock> guard(spinlock);
sum--;
}
}
int main()
{
std::thread th_1(Add);
std::thread th_2(Sub);
th_1.join();
th_2.join();
cout << sum << endl;
return 0;
}
위와 같은 코드가 된다. SpinLock에 대해서는 위에 써 놓았지만 만약 SpinLock 이 서로 경합이 붙어 무한루프를 돌기 시작하면 CPU 점유율이 확 높아진다. 왜냐하면 컨택스 스위칭이 되어 내실행 소유권을 넘겨주면서 다른쓰레드가 적절히 활용하게 되지만 SpinLock이 계속 무한정 루프를 돌며 내가 가져갈수 있나를 체크하면 CPU를 쓸데없이 낭비하는 경우 이기도 하다. 이런부분을 종합해서 SpinLock을 활용해야 하겠다.
댓글