락을 구현하는 3가지 방식 중 하나인 스핀락에 대해 알아보고 이를 예제로 살펴보겠습니다.
스핀락은 자원에 접근하기 위해 무한히 대기하여 락을 구현하는 방식입니다.
따라서 이론적으로 다음과 같이 구현할 수 있을 것입니다.
1) 첫 번째 시도
class SpinLock
{
volatile bool _key = false;
public void Accquire()
{
// 잠금이 풀릴때까지 대기
while (_key)
{
}
// 잠금 설정
_key = true;
}
public void Release()
{
// 잠금 해제
_key = false;
}
}
class Program
{
static int _num = 0;
static SpinLock _lock = new SpinLock();
static void Thread_1()
{
for (int i = 0; i < 100000; i++)
{
_lock.Accquire();
_num++;
_lock.Release();
}
}
static void Thread_2()
{
for (int i = 0; i < 100000; i++)
{
_lock.Accquire();
_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);
}
}
스핀락이 제대로 구현되었다면 실행결과값은 0이 나와야 합니다.
실행결과를 살펴보면 다음과 같은 실행결과를 볼 수 있습니다.
이는 의도했던 결과가 아니고 또 결과로 보아 스핀락이 제대로 동작하지 않았음을 확인할 수 있습니다.
위와 같은 일이 발생한 이유는 Accquire 함수에서
계속 대기하다가 풀린 자원에 잠금을 다시 설정하는 과정이 동시에 일어나지 않고 쪼개져 일어나기에 발생합니다.
따라서 이 과정을 한번에 처리해야만 합니다.
그리고 C#에서는 이를 위해 함수를 지원하고 있습니다.
2) 두 번째 시도 - Interlocked.Exchange
Interlocked.Exchange 함수는 여러 버전이 존재하는데, 예제에서는 다음과 같이 사용합니다.
Interlocked.Exchange(ref int 원본, int 기대값)
위의 함수는 동작하게되면 원본을 기대값으로 초기화 합니다.
하지만 리턴되는 값은 초기화되기 이전의 원본값을 리턴하게 됩니다.
이를 사용해 이전의 예제 코드를 개선해보겠습니다.
class SpinLock
{
volatile int _key = 0;
public void Accquire()
{
while (true)
{
int original = Interlocked.Exchange(ref _key, 1);
if (original == 0)
break;
}
}
public void Release()
{
_key = 0;
}
}
class Program
{
static int _num = 0;
static SpinLock _lock = new SpinLock();
static void Thread_1()
{
for (int i = 0; i < 100000; i++)
{
_lock.Accquire();
_num++;
_lock.Release();
}
}
static void Thread_2()
{
for (int i = 0; i < 100000; i++)
{
_lock.Accquire();
_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);
}
}
위와 같이 개선하였습니다.
_key를 1로 설정하면 계속 대기하고, 0으로 설정하면 대기를 멈추게 됩니다.
실행결과는 다음과 같습니다.
스핀락이 문제없이 동작하여 위와 같이 0이 출력되는 것을 볼 수 있습니다.
그런데 Accquire 함수의 동작 구조를 보면 비교와 값을 바꾸는 로직이 나뉘어져 있습니다.
이를 하나로 합치면 훨씬 더 빠른 연산이 가능하리라 생각됩니다.
그리고 다행이도 C#은 이를 위한 함수도 지원하고 있습니다.
3) 최종 시도 - Interlocked.CompareExchange
Interlocked.CompareExchange 함수도 여러 버전이 존재하는데, 예제에서는 다음의 버전을 사용합니다.
Interlocked.CompareExchange(ref int 원본, int 기대값, int 비교값)
원본과 비교값을 비교하여 같다면 원본을 기대값으로 초기화합니다.
하지만 반환되는 값은 초기화 이전의 원본의 값을 반환하는 함수입니다.
이를 통해 예제 코드를 수정해보겠습니다.
class SpinLock
{
volatile int _key = 0;
public void Accquire()
{
while (true)
{
int expected = 0;
int desired = 1;
if (Interlocked.CompareExchange(ref _key, desired, expected) == expected)
break;
}
}
public void Release()
{
_key = 0;
}
}
class Program
{
static int _num = 0;
static SpinLock _lock = new SpinLock();
static void Thread_1()
{
for (int i = 0; i < 100000; i++)
{
_lock.Accquire();
_num++;
_lock.Release();
}
}
static void Thread_2()
{
for (int i = 0; i < 100000; i++)
{
_lock.Accquire();
_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);
}
}
CompareExchange 함수를 통해 비교와 값을 바꾸는 연산을 한번에 수행하도록 하였습니다.
이를 통해 _key가 0이 되면 대기를 멈추고, 1이 되면 계속 대기하게 됩니다.
이제 실행 결과를 살펴보겠습니다.
스핀락이 성공적으로 구현된 것을 확인할 수 있습니다.
'C#' 카테고리의 다른 글
Event를 통한 Lock 구현 (0) | 2024.12.04 |
---|---|
Context Switching (0) | 2024.12.04 |
DeadLock과 Lock구현 (0) | 2024.12.02 |
Lock 기초 (1) | 2024.12.02 |
Interlocked (0) | 2024.11.27 |