[ 멀티쓰레드 ] Spinlock구현
c#으로 서버강의를 들으며 스핀락을 직접 구현했다.
나는 C++을 주로 사용하므로 C#으로 구현한 스핀락의 구조를 C++ 언어에서 적용할 수 있게 공부하고자 함.
사실 구현은 아주 쉽지만 어떤 부분을 놓칠 수 있는지 멀티쓰레드를 다루려면 어떤 부분에 민감해야 하는지에 대한 포스팅이 될 것 같다.
스핀락이란?
쓰레드의 컨텍스트 스위칭을 방지하기 위한 락이다.
락은 기본적으로 자원 확보가 되지 않을 시 우선순위가 높은 쓰레드를 실행하게 된다. 이는 컨텍스트 스위칭이 발생한다는 것인데 컨텍스트 스위칭이 빈번하게 발생하면 성능저하가 일어나므로 멀티쓰레드 환경에서는 잦은 컨텍스트 스위칭을 방지하는 것이 성능 향상에 주된 요인이 된다.
기본적인 구현은 쓰레드가 자원을 확보할 때까지 while문을 돌면서 CPU를 계속 점유하고 있으면 된다.
<옳은 코드>
class SpinLock
{
volatile int _locked = 0;
public void Acquire()
{
while (true)
{
int original = Interlocked.Exchange(ref _locked, 1);
if (original == 0)
break;
}
}
public void Release()
{
_locked = 0;
}
}
위가 C#의 스핀락 코드.
눈여겨 볼 곳은 Acquire 함수 내부의 while 부분이다.
멀티쓰레드 환경에서는 기존에 작성하던 코드가 오류를 빚어낼 수 있다.
자원을 획득 할 때까지, 위의 코드에서 보면 volatile int _locked가 0이 아니고 1일 때 자원을 획득했다고 판단할 수 있다. 그러면 위의 코드가 아니라
<틀린 코드>
class SpinLock
{
volatile int _locked = 0;
public void Acquire()
{
while (true)
{
if(_locked == 1)
break;
}
}
public void Release()
{
_locked = 0;
}
}
위의 코드처럼 실행해도 똑같이 수행 되어야 할 것이다.
답부터 말하자면 위의 코드는 if(_locked == 1) 원자성을 해치게 되므로 정상적으로 작동하지 않는다.
원자성이란 코드가 어셈블리 언어로 변환되면서 내부적으로 코드 재배치가 일어나게 되고, 이 때문에 코드가 순차적으로 실행되지 않게되는 것이다.
예를 들면 A에서 lock을 1으로 만들어서 A에서만 break만 해제해야 하는데 B가 갑자기 끼어들어 자신도 break가 해제되는 상황이 나오게 된다.
따라서 원자성을 지켜줄 수 있는 인터락 계열의 함수가 필요하거나 메모리 펜스가 필요하게 된다. (race condition 이라고도 함.) C++에서는 atomic을 적용한 변수를 사용할 수도 있다. 다만 atomic은 값 복사와 대입 연산이 불가하다는 점을 주의하자.
< atomic 의 사용>
#include <iostream>
#include <thread>
#include <atomic>
using namespace std;
void func1(std::atomic<int>& i) {
for (int j = 1; j <= 1000000; ++j)
i++;
}
int main() {
std::atomic<int> i = 0;
thread t1(func1, std::ref(i));
thread t2(func1, std::ref(i));
t1.join();
t2.join();
cout << i << endl;
}
따라서 위쪽의 C#코드를 C++로 수정하고자 한다면 다음의 함수를 사용할 수 있겠다.
atomic<bool> Locked = false;
bool t = true;
bool f = false;
bool result = Locked.compare_exchange_strong(t, f);
compare_exchange_strong 함수의 내부는 다음과 같이 생겼다.
bool compare_exchange_strong(_TVal& _Expected, const _TVal _Desired,
const memory_order _Order = memory_order_seq_cst) noexcept { // CAS with given memory order
char _Expected_bytes = _Atomic_reinterpret_as<char>(_Expected); // read before atomic operation
char _Prev_bytes;
위에서 언급한 메모리 펜스 종류중 하나인 memory_order_seq_cst 를 사용하는 것을 볼 수 있다.
정리하면 스핀락의 구현은 메모리 펜스가 핵심적인 요소이며 메모리 펜스는 원자성을 지키기 위함인 것으로 정리할 수 있을것이다.