스핀 락
스핀락은 폴링 방식의 동기화 처리를 하는 방식이다.
마이크로 프로세서 에서 내부 인터럽트를 사용하지 않고 값의 state 가 바뀌는지
while(1) 을 이용하여 확인 하는것과 같다.
한 가지 예를 보자
num 이라는 데이터를
첫번째 스레드에선 +1 씩 계속 증가시키고
두번째 스레드에선 -1 씩 계속 감소시키는 행위를 각각 10만번 씩 하면
직관적으로는 당연히 프로그램이 뻗지 않는이상 당연히 0이 나올 것이다.
하지만 이전 포스팅에서 자원 접근에 대한 동기화가 이루어 지지 않으면 한 쪽에서 값을 변경 시켜도
다른 쪽에선 이전에 가지고 있던값을 최신이라고 저장하고 있다가 값을 변경시키는 현상이 발생하므로
의도했던 데이터 갱신이 이루어지지 않는다.
이를 해결하기 위해 첫번째 폴링 방식의 스핀락을 구현할때 발생할 수 있는 문제점과
이를 해결하기 위한 C#에서 제공되는 Interlock의 클래스를 이용한 예를 살펴보자.
class SpinLock
{
volatile int locked = 0;
public void Acquire()
{
while(true)
{
if (locked == 1)
break;
}
locked = 1;
}
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); //0인지 확인
}
}
while(true)
{
if (locked == 1)
break;
}
locked = 1;
이 코드를 보면 마치 둘 중에 누군가 먼저 도착하는 스레드가 locked을 잡은다음 값을 1로 바꾸고
다음 스레드가 lcoked이 0인동안 (먼저 온 스레드가 사용중) 대기하고 있다가 locked이 1로 풀리면
사용 하면 될거 같다.
하지만, 이 부분에서도 문제가 되는 부분은 저 while 문 내부의 코드이다.
어떠한 상황이 발생 할 수 있는지 살펴보면
둘이 동시에 locked에 접근하여 0인 state를 갖는것이다. 이렇게 되면
첫번째 스레드도, 두번째 스레드도,
"음 누군가 사용 하고 있군 존버해야겠다."
라고 두 스레드 모두 존버하므로 교착상태에 빠지게 된다.
이 동시성 제어를 해결하기 위해
C# 에서 Interlocked 클래스에서 제공되는
Exchage를 사용해서 해결한다.
물론 이전 포스팅의 Increment도 메모리 베리어를 사용하며 증가 감산의 동시성 제어를 할 수 있다.
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); //0인지 확인
}
}
실제 출력을 해보면
교착상태에 빠지지 않고 값이 정상적으로 0이 출력이 된다.
while(true)
{
int original_value = Interlocked.Exchange(ref locked, 1);
if (original_value == 0) //만약 원래 값이 0이라면 다른 스레드가 사용하지 않았다고 판단.
break;
}
Exchage를 살펴보면
// 요약:
// 원자 단위 연산으로 부호 있는 32비트 정수를 지정된 값으로 설정하고 원래 값을 반환합니다.
//
// 매개 변수:
// location1:
// 지정된 값으로 설정할 변수입니다.
//
// value:
// location1 매개 변수의 설정 값입니다.
//
// 반환 값:
// location1의 원래 값입니다.
//
// 예외:
// T:System.ArgumentNullException:
// location1의 주소는 null 포인터입니다.
이렇게 요약 되어있는것을 확인할 수 있는데
첫번째 매개변수에는 우리가 자물쇠라고 보통표현하는 잠금장치 값 을 설정해주고
두번째 매개변수에는 우리가 바꿀 값을 사용해준다.
가장 중요한건 반환값인데 여기서 반환값은 우리가 1로 바꾸기 전의 locked에 있는 값이다.
따라서 original_value가 0으로 반환 되었다는것은 다른 스레드가 locked을 1로 바꾸기 전,
즉 , 현재 진입한 스레드가 자물쇠를 얻을 수 있다는 말이 된다.
그래서 만약, 어떤 스레드가 최초로 저 while 루프를 진입하면
"값이 1로 바꾸고 싶어, 그리고 기존 값이 0이면 내가 바꾸고 while루프를 나갈꺼야"
로 해석 된다.
그럼 두번째 온 스레드가 첫 진입한 스레드가 작업을 마치지 않았다면 original 값은 1로 바뀌어 있을테고
첫 진입한 스레드가 Release() 함수를 실행하기 전까지 while문에서 무한정 대기할 것이다.
Interlocked의 Exchange와 비슷한 CompareExchage가 있다.
이름만 봐도 뭔가를 비교해서 바꾸는 함수 같다.
실제 그렇다.
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 locked, desire, expected) == expected)
break;
}
}
public void Release()
{
locked = 0;
}
}
CompareExchage를 보면
첫 번째 파라미터는 자물쇠의 역할이고,
두 번째 파라미터는 바꾸고자 하는값,
세 번째 파라미터는 locked과 비교하는 값이다.
따라서, 만약, locked과 expcted값이 같다면 desire로 값으로 바꾸고
반환 값은 Exchage와 똑같이 원래의 값을 반환한다.
좀더 설명을 부연하면,
만약 , locked와 expected값이 0으로 같다 ( 아무도 자물쇠 (locked)를 사용하지 않은 상태이다)
그럼 원래 값 0을 반환시키고 locked을 1로 바꾼다. (다음 순서의 스레드들은 대기하게 된다 1인상태이므로)
'Programing_Language > C#' 카테고리의 다른 글
[쓰레드 동기화] Interrupt(이벤트) 방식의 AutoResetEvent (0) | 2020.07.21 |
---|---|
[쓰레드 동기화] Thread [Sleep, Yield] , ContextSwitching (0) | 2020.07.21 |
[쓰레드 동기화] Monitor 와 lock (IPC) (lock > Monitor 편의성) (0) | 2020.07.18 |
[쓰레드 동기화] Interlocked (공유자원과 임계구역) (0) | 2020.07.18 |
[쓰레드] TaskCreateOptions.LongRunning (0) | 2020.07.17 |
댓글