Q1. 락을 구현할 때 Interlocked.CompareExchange를 이용하는 이유는 무엇인가?
Q2. 스핀락을 안보고 구현해 보세요.
namespace ServerCore
{
class SpinLock
{
volatile bool _locked = false;
public void Acquire()
{
while(_locked)
{
// 잠김 풀리기를 기다린다
}
// 내꺼!
_locked = true;
}
public void Release()
{
_locked = false;
}
}
internal class Program
{
static int _num = 0;
static SpinLock _lock = new SpinLock();
static void Thread_1()
{
for(int i = 0; i<100000; i++)
{
_lock.Acquire();
_num++;
_lock.Release();
}
}
static void Thread_2()
{
for (int i = 0; i < 100000; i++)
{
_lock.Acquire();
_num--;
_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);
}
}
}
만든 Spinlock이 제대로 구현된 거라면 0이 출력될 것이다.
이상한 값이 나온다.
스핀락이 제대로 작동을 안하고 있다고 결론을 내릴 수 있다.
거의 동시에 들어가면
공동 승자가 된다.
들어 온 다음에 문을 잠궈서 서로 락을 획득했다고 뿌듯해 하는 상황
이상하긴 해. 두명이 화장실에 있는데 문을 잠근거.
애당초 문에 들어 간 다음에 문을 잠그는 거 까지 하나의 행동으로 이루어져야지 들어 간 다음에 잠그겠다는 두가지 동작으로 쪼개지면 안된다.
멀티스레드에서는 원자적으로 동작이 이루어져야 된다는게 중요한 개념.
동시에 들어가는 상황을 원천적으로 차단해야 한다.
SpinLock에서
public void Acquire()
{
while(_locked)
{
// 잠김 풀리기를 기다린다
}
// 내꺼!
_locked = true;
}
이 부분이 문제가 되고 있다.
while 부분은 화장실 문이 비어 있을 때 까지 계속 대기
_locked =true는 들어가서 문을 잠그는 부분
이걸 따로 하고 있으니까 두개의 thread가 동시에 들어와서 동시에 통과하는 경우가 문제가 된다.
그래서 한번에 실행되게 바꿔야 한다.
num++에선 interlocked 계열 함수 이용했어.
일단 Exchange를 보면 location1, value를 받고 있는데 int를 받는 버전을 사용할거야.
bool아닌 int로 작업하게 될거. c++에서는 boolean 버전도 마련되어 있어.
volatile int _locked = 0;
bool을 int로 바꾸고 false는 0으로 바꿔치기했다.
int original = Interlocked.Exchange(ref _locked, 1);
Exchange에서 반환값이 1을 넣어주기 전의 값이 반환된다. original이 0이면 아무도 없었다는 거고 1이면 이미 잠겨있는 걸 다시 잠근거
public void Acquire()
{
while (true)
{
int original = Interlocked.Exchange(ref _locked, 1);
if (original == 0)
break;
}
}
뺑뺑이 돌다가 잠겨있지 않은 상태를 잠그게 되면 break 한다.
잘 되는지 테스트 해보자.
이제 0이 나온 것을 볼 수 있다.
여기서 또 유심히 봐야할 것은 _locked는 ref를 붙였는데 공유해서 경합해서 사용하는 애기 때문에 멋대로 값을 읽어서 사용하면 안된다. 그럼에도 if문으로 비교했는데 original은 stack에 있는 경합하지 않는 하나의 스레드에서만 사용하는 값이므로 문제가 없다.
멀티스레드 쓰려면 그린존, 레드존을 구분하는 눈이 필요하다.
의사코드로 보면
int original = _locked;
_locked = 1;
if(original == 0)
break;
이런 문장 쓴거야. 싱글스레드였으면 이런 표현.
근데 이렇게 코드를 두줄에 걸쳐서 하면 아까와 같은 문제가 일어나니까 한번에 실행되게 묶어준게 Interlocked.Exchange라고 보면 된다.
그럼에도 직관적이 아닌게 마음에 걸려.
if(_locked == 0)
_locked = 1;
이런 코드가 더 깔끔할 거 같은데 그런 버전도 마련이 되어 있다. 대부분은 이 후자 버전을 사용해.
또 다른 버전을 알아보자
첫 인자랑 comparand랑 비교해서 같다면 두번째 인자인 value를 location1에다가 넣어주게 될거야. 뱉어주는 건 exchange와 마찬가지로 original value를 뱉어준다.
이런 계열의 함수를 CAS Compare-And-Swap계열 함수라고 한다. C++에도 이런 계열 마련되어 있다. 인터페이스는 다르니 읽어보고 쓰면 된다.
if(_locked == 0)
_locked = 1;
이게 하고 싶었던 거였으니까
Interlocked.CompareExchange(ref _locked, 1, 0);
이렇게 넣어주면 위의 코드와 같게 된다.
public void Acquire()
{
while (true)
{
//int original = Interlocked.Exchange(ref _locked, 1);
//if (original == 0)
// break;
// CAS Compare-And-Swap
int original = Interlocked.CompareExchange(ref _locked, 1, 0);
if (original == 0)
break;
}
}
근데 이렇게 보면 헷갈린다. _locked랑 뭐를 비교하는지 헷갈려.
그래서 C++에서 사용하던 바식으로 사용하는 것을 좋아해.
public void Acquire()
{
while (true)
{
//int original = Interlocked.Exchange(ref _locked, 1);
//if (original == 0)
// break;
// CAS Compare-And-Swap
int expected = 0;
int desired = 1;
int original = Interlocked.CompareExchange(ref _locked, desired, expected);
if (original == 0)
break;
}
}
예상값(expected)은 0이고 그 값이 맞다면 desired를 넣어주겠다고 하면 가독성이 좋을 거 같아.
public void Acquire()
{
while (true)
{
//int original = Interlocked.Exchange(ref _locked, 1);
//if (original == 0)
// break;
// CAS Compare-And-Swap
int expected = 0;
int desired = 1;
if (Interlocked.CompareExchange(ref _locked, desired, expected) == expected)
break;
}
}
이렇게 정리해 줄 수 있다.
테스트 해보면 0이라는 값이 잘 나온다.
for문의 반복값을 늘려서 해봐도 0으로 잘 출력되는 것을 볼 수 있다.
이게 간단하게 구현한 스핀락의 구현
락을 획득할 때 까지 무한정 트라이 하는 방법
public void Release()
{
_locked = 0;
}
여기서는 별도의 처리를 안해줘도 된다.
Acquire를 해서 통과했다는 건 유일하게 _locked를 고 있다는 말이 되니까 문 열어주는 작업은 설렁 혼자 하는 작업이기 때문에 이렇게 별도 처리 하지 않고 0만 넣어주면 처리가 된다.
스핀락 구현 해보셨나요? 라는 질문에 당당하게 대답할 수 있다.
using System;
using System.Threading;
using System.Threading.Tasks;
namespace ServerCore
{
class SpinLock
{
volatile int _locked = 0;
public void Acquire()
{
while (true)
{
//int original = Interlocked.Exchange(ref _locked, 1);
//if (original == 0)
// break;
// CAS Compare-And-Swap
int expected = 0;
int desired = 1;
if (Interlocked.CompareExchange(ref _locked, desired, expected) == expected)
break;
}
}
public void Release()
{
_locked = 0;
}
}
internal class Program
{
static int _num = 0;
static SpinLock _lock = new SpinLock();
static void Thread_1()
{
for(int i = 0; i<1000000; i++)
{
_lock.Acquire();
_num++;
_lock.Release();
}
}
static void Thread_2()
{
for (int i = 0; i < 1000000; i++)
{
_lock.Acquire();
_num--;
_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);
}
}
}
'Server programming' 카테고리의 다른 글
01_12_멀티쓰레드_AutoResetEvent (0) | 2023.03.16 |
---|---|
01_11_멀티쓰레드_ContextSwitching (0) | 2023.03.15 |
01_09_멀티쓰레드_Lock 구현 이론 (0) | 2023.03.14 |
01_08_멀티쓰레드_DeadLock (0) | 2023.03.14 |
01_07_멀티쓰레드_Lock 기초 (0) | 2023.03.13 |
댓글