
스핀락
스핀락은 스핀락은 임계 영역(critical section)에 접근하기 위해 락을 획득할 때까지 CPU를 멈추지 않고 계속 루프를 돌면서 대기하는 방식의 동기화 기법이다.
장점으로는 '컨텍스트 스위칭이 발생하는 다른 동기화 기법보다 오버헤드가 낮다.'
단점으로는 '다수의 쓰레드가 경쟁할 경우, CPU 사용률이 급격히 증가할 수 있다.'
스핀락기법을 사용할 때 컨텍스트 스위칭 비용이 다른 동기화 기법들 보다 낮은 이유는 System Call 호출을 통해 커널 모드로의 전환이 발생하지 않기 때문이다.
예로 this_thread::sleep_for(std::chrono::milliseconds(100)); 를 호출하게 되면 쓰레드는 '커널 모드'로의 전환이 발생한다.
OS는 현재 쓰레드를 대기 목록에 추가하고 Blocked 상태로 만든다.(아래 그럼에서는 waiting)

지정한 시간(100ms)가 지나게 되면 OS가 쓰레드를 다시 ready상태로 만들고 CPU 스케쥴러가 적절한 시점에 실행할 수 있도록 한다. 이때 다시 커널 모드 -> 유져 모드로의 전환이 필요하다.
커널 모드 동기화기법을 사용하면 CPU가 그동안 다른일을 함으로써 CPU점유율을 낮출 수 있다는 장점도 있지만 반대로 느리다는 단점도 있다. (문맥 교환 비용때문에)
스핀락은 이런 모드 전환을 하지 않고 유져 모드에서 존버?를 하는 방법이기 때문에 대기 시간이 아주 짧은 (여러 조건에 맞는 상황에서) 상황에서 사용하면 유용한 동기화 기법이다.
아래 코드를 통해 Atomic이 뭔지 이해하고 스핀락을 간단하게 구현 해보도록 하겠다.
기존 구현의 문제점
class SpinLock
{
public:
void lock()
{
while (_locked) {}
_locked = true;
}
void unlock()
{
_locked = false;
}
private:
bool _locked = false;
};
int32 sum = 0;
SpinLock spinLock;
void Add()
{
for (int32 i = 0; i < 100000; ++i)
{
lock_guard<SpinLock> guard(spinLock);
sum++;
}
}
void Sub()
{
for (int32 i = 0; i < 100000; ++i)
{
lock_guard<SpinLock> guard(spinLock);
sum--;
}
}
int main()
{
std::thread t1(Add);
std::thread t2(Sub);
t1.join();
t2.join();
cout << sum << endl;
return 0;
}
코드는 두개의 쓰레드를 생성하여 sum이라는 변수를 100000더하고 빼는 코드이다. 예상되는 값은 0을 원하지만 엉뚱한 값이 나오게 된다.
스핀락을 단순히 bool 변수로 구현하면, 두 개의 쓰레드가 동시에 _locked = true를 설정하는 상황이 발생할 수 있다.
이 경우 임계 영역이 보호되지 않아 원하지 않는 결과가 나올 수 있다.
해결 방법: CAS(Compare-And-Swap) 활용
CAS는 특정 메모리 위치의 값을 비교한 후, 일치하는 경우에만 새로운 값으로 교체하는 원자적 연산이다.
C++에서는 std::atomic 클래스의 compare_exchange_strong 함수를 사용하여 CAS를 구현할 수 있다.
순서는 다음과 같다.
1. _locked가 false인 경우 true로 설정하여 락을 획득한다.
2. 락을 얻지 못한 경우 다시 false로 설정한 뒤 재시도한다.
3. 임계 영역이 끝나면 _locked를 false로 설정하여 락을 해제한다.
class SpinLock
{
public:
void lock()
{
bool expected = false;
bool desired = true;
while (_locked.compare_exchange_strong(expected, desired) == false)
{
expected = false; // <- 이부분 주의, 다시 false로 만들어주어야 한다.
}
}
void unlock()
{
// _locked = false;
_locked.store(false);
}
private:
atomic<bool> _locked = false;
};
int32 sum = 0;
SpinLock spinLock;
void Add()
{
for (int32 i = 0; i < 100000; ++i)
{
lock_guard<SpinLock> guard(spinLock);
sum++;
}
}
void Sub()
{
for (int32 i = 0; i < 100000; ++i)
{
lock_guard<SpinLock> guard(spinLock);
sum--;
}
}
expected는 예상되는 값 desired는 기대되는 값이다. 두 쓰레드 A, B중 A가 먼저 lock을 거는 경우
compare_exchange_strong함수는 _locked를 true로 변경하고 true를 반환한다.
나중에 도착한 B는 compare_exchange_strong호출시 현재 _locked의 값이 false이기 때문에 compare_exchange_strong 함수는 false를 반환하게 된다.
정리
이렇게 간단하게 CAS를 적용한 스핀락을 구현해보았다.
스핀락구현시 CAS 사용하면 멀티스레드 환경에서도 안전하게 동기화할 수 있지만 위의 장단점을 잊지말자.
'서버 & 네트워크' 카테고리의 다른 글
[네트워크] Loopback Address란? (1) | 2024.02.29 |
---|---|
[네트워크] OSI 7 Layer란? (0) | 2024.02.21 |
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!