스핀락은 폴링 방식의 동기화 처리를 하는 방식이다
마이크로 프로세서 에서 내부 인터럽트를 사용하지 않고 값의 state 가 바뀌는지 while(1) 을 이용하여 확인 하는 것 과 같다
예를 들어 num 이라는 데이터를 첫 번째 스레드 에선 +1씩 계속 증가 시키고 두 번째 스레드 에선 -1씩 계속 감소 시키는 행위를 각각 10만번씩 하면 직관적으로는 당연히 프로그램이 뻗지 않는 이상 당연히 0이 나올 것 이다 하지만 자원 접근에 대한 동기화가 이루어 지지 않으면 한 쪽에서 값을 변경 시켜도 다른 쪽에선 이전에 가지고 있던 값을 최신이라고 저장 하고 있다가 값을 변경시키는 현상이 발생 하므로 의도했던 데이터 갱신이 이루어지지 않는다
이를 해결 하기 위해 첫 번째 폴링 방식의 스핀 락을 구현 할 때 발생할 수 있는 문제점과 이를 해결하기 위한 C# 에서 제공되는 Interlock 의 클래스를 이용한 예를 살펴보자
class SpinLock
{
volatile int locked = 0;
public void Acquitre()
{
while(true)
{
if(lock == 1)
break;
}
locked = 1;
}
public void Release()
{
locked = 0;
}
}
class Program
{
static int num = 0;
static SpinLock _lock = new SpintLock();
static void Thread_1()
{
for(int i = 0; i<100000; ++i)
{
_lock.Acquire(); //(locked == true);
num -=1;
_lock.Release(); // (locked == false) lock 해제
}
}
static void Thread_2()
{
for( int i = 0; i < 100000; ++i)
{
_lock.Acquire();
num -= 1;
_lock.Release();
}
}
static void Main(string[] args)
{
Task t1 = new Task(Thread_1);
Task t2 = new Task(Thread_2);
t1.Start();
t2.Start();
Task.WaiAll(t1,t2);
Console.WriteLine(num); // 0인지 확인
}
}
이 코드를 보면 마치 둘 중에 누군가 먼저 도착하는 스레드가 locked을 잡은 다음 값을 1로 바꾸고 다음 스레드가 locked 이 0인 동안 대기하고 있다가 locked이 1로 풀리면 사용하면 될 거 같지만 위 코드에서 문제가 되는 부분은 while문 내부의 코드이다
위 코드는 두 스레드가 동시에 locked에 접근하여 0인 state를 갖는 것이다 이렇게 되면 두 스레드 모두 누군가 사용 하고 있는 것으로 생각해 교착 상태에 빠지게 된다 이 동시성 제어를 해결하기 위해 Interlocked 클래스에서 제공되는 Exchage를 사용해 해결한다
Interlocked의 Exchange에서 작동 되는 예
class SpinLock
{
volatile int locked = 0;
public void Acquire()
{
while (true)
{
int original_value = Interlocked.Exchange(ref locked, 1);
if (original_value == 0) // 만약 원래 값이 0이라면 다른 스레드가 사용하지 않았다고 판단
break;
/*
while(locked) // 폴링 상대로 잠김 대기
// lock 획득
locked = true; */
}
}
public void Release()
{
locked = 0;
}
}
class Program
{
static int num = 0;
static SpinLock _lock = new SpinLock();
static void Thread_1()
{
for (int i = 0; i < 100000; ++i)
{
_lock.Acquire(); // (locked == true);
num += 1;
_lock.Release(); // (locked == false) lock 해제
}
}
static void Thread_2()
{
for (int i = 0; i < 100000; ++i)
{
_lock.Acquire();
num -= 1;
_lock.Release();
}
}
static void Main(string[] args)
{
Task t1 = new Task(Thread_1);
Task t2 = new Task(Thread_2);
t1.Start();
t2.Start();
Task.WaitAll(t1, t2);
Console.WriteLine(num);
}
}
}
Exchange 를 살펴보면
첫 번째 매개변수에는 우리가 자물쇠라고 보통 표현하는 잠금 장치 값을 설정해주고 두 번째 매개변수에는 우리가 바꿀 값을 사용해준다.
가장 중요 한 건 반환 값 인데 여기서 반환 값은 우리가 1로 바꾸기 전의 locked에 있는 값이다
따라서 original_value가 0으로 반환 되었다 는 건 다른 스레드 가 locked을 1로 바꾸기 전 현재 진입한 스레드 가 자물쇠를 얻을 수 있다는 말이 된다 그래서 만약 어떤 스레드가 저 while 루프로 진입 하면
값이 1로 바뀌고 기존 값이 0이면 자기가 바꾸고 while 루프를 나가게된다
그럼 두 번째 온 스레드가 첫 진입한 스레드가 작업을 마치지 않았다면 original 값은 1로 바뀌어 있을 테고 첫 진입한 스레드가 Release() 함수를 실행하기 전까지 while 문에서 무한정 대기 할 것이다
CompareExchange
class SpinLock
{
volatile int locked = 0;
public void Acquire()
{
while (true)
{
//CAS Compare=AND-Swap
int expected = 0;
int desire = 1;
if(Interlocked.CompareExchange(ref desire, expected) == expected)
break;
}
public void Release()
{
locked = 0;
}
}
CompareExchange 를 보면
첫 번째 파라미터는 자물쇠의 역할이고
두 번째 파라미터는 바꾸고자 하는 값
세 번째 파라미터는 locked과 비교하는 값이다
따라서 만약 locked과 expcted 값이 같다면 desire로 값이 바뀌고
반환 값은 Exchange와 똑같이 원래의 값을 반환한다
'개인 스터디' 카테고리의 다른 글
비동기 TCP 서버 (0) | 2024.10.17 |
---|---|
Monitor (0) | 2024.10.16 |
C# Thread(스레드) 주요 속성과 메서드 (0) | 2024.10.15 |
무명 메서드 (Anonymous Method) (0) | 2024.10.14 |
TCP 서버 (0) | 2024.10.11 |