문제 코드
namespace ServerCore;
internal class SpinLock
{
private volatile int _locked;
public void Enter()
{
while (_locked == 0)
{
}
_locked = 1;
}
public void Leave()
{
_locked = 0;
}
}
internal class Program
{
static int _num = 0;
static SpinLock _lock = new SpinLock();
public static void Thread_1()
{
for (int i = 0; i < 100000; i++)
{
_lock.Enter();
_num++;
_lock.Leave();
}
}
public static void Thread_2()
{
for (int i = 0; i < 100000; i++)
{
_lock.Enter();
_num--;
_lock.Leave();
}
}
private static void Main(string[] args)
{
var t1 = new Task(Thread_1);
var t2 = new Task(Thread_2);
t1.Start();
t2.Start();
Task.WaitAll(t1, t2);
Console.WriteLine(_num);
}
}
이 코드의 주요 문제점은 SpinLock
의 Enter
메서드 구현에 있다. 현재 구현은 Enter
메서드가 적절한 방법으로 원자성을 보장하지 않기 때문에 다중 스레드 환경에서 안전하지 않다. _locked == 0
조건을 검사하고 _locked = 1
로 설정하는 과정이 원자적으로 실행되지 않아 경쟁 상태(race condition)가 발생할 수 있다.
이로 인해 다음과 같은 상황이 발생할 수 있다:
- 두 스레드가 동시에
_locked == 0
조건을 만족하고,_locked = 1
로 변경되기 전에 다른 스레드가 끼어들어_locked
값을 조작할 수 있다. - 결과적으로
SpinLock
이 제대로 작동하지 않고 여러 스레드가 동시에 임계 구역에 들어가게 된다.
해결 방법
이 문제를 해결하기 위해 Interlocked.Exchange
메서드를 사용하여 _locked
값을 원자적으로 교환해야 한다. Interlocked.Exchange
는 스레드 간 원자성을 보장하므로 경쟁 상태를 방지할 수 있다.
수정된 Enter
메서드는 다음과 같다:
public void Enter()
{
// 원자적 교환을 통해 잠금 상태를 설정한다.
while (Interlocked.Exchange(ref _locked, 1) == 1)
{
// 다른 스레드가 잠금을 가지고 있는 동안 대기한다.
}
}
while (true)
루프 안에 있는 Interlocked.Exchange
메서드는 스레드가 _locked
변수를 1로 설정하는 원자적 연산을 수행하며, 현재 _locked
의 값에 따라 잠금 상태를 제어한다.
코드의 주요 부분을 다시 보면 다음과 같다:
while (true)
if (Interlocked.Exchange(ref _locked, 1) == 0)
return;
이 코드의 동작을 하나씩 설명하면:
Interlocked.Exchange
:Interlocked.Exchange(ref _locked, 1)
는_locked
변수의 값을1
로 변경하는 동시에,_locked
의 이전 값을 반환한다. 이 연산은 원자적으로 수행되기 때문에 여러 스레드가 동시에 이 연산을 실행해도 충돌 없이 동작한다.- 잠금 확인과 설정:
_locked
의 이전 값이0
(잠기지 않은 상태)일 경우,Interlocked.Exchange
는0
을 반환하고_locked
를1
로 설정한다. 이 상태에서는 현재 스레드가 잠금을 획득하게 되므로,return
을 통해Enter
메서드가 종료된다._locked
의 이전 값이1
(이미 잠금 상태)일 경우,Interlocked.Exchange
는1
을 반환한다. 이 경우if
조건이 만족되지 않아return
하지 않고while
루프가 반복된다.
- 루프 동작: 다른 스레드가
Leave
메서드를 통해_locked
를0
으로 설정할 때까지 이while
루프는 계속 반복된다._locked
가0
이 되면Interlocked.Exchange
에서0
을 반환하고_locked
를1
로 변경하면서 잠금을 획득하게 된다.
간단히 요약하면
Interlocked.Exchange(ref _locked, 1)
는_locked
값을 원자적으로1
로 설정하면서 이전 값을 반환하여,_locked
가0
인지 확인하고1
로 설정하는 작업을 원자적으로 수행한다._locked
가0
일 때만 스레드가 잠금에 성공하고, 그렇지 않으면while
루프를 통해 잠금이 풀릴 때까지 대기하게 된다.
이렇게 구현된 코드 구조는 여러 스레드가 동시에 잠금에 접근하려 할 때 안전하게 하나의 스레드만 잠금을 획득하도록 보장한다.
원자적? 원자성?
'원자적' 혹은 '원자성'은 컴퓨터 과학에서 어떤 작업이 더 이상 분할될 수 없는 최소 단위로 수행된다는 것을 의미한다. 즉, 작업이 중간에 중단되거나 다른 작업이 끼어들 수 없는 상태를 말한다. 원자적 연산은 반드시 완전히 실행되거나 전혀 실행되지 않는 방식으로 수행된다.
'원자적'이라는 표현은 화학의 '원자'에서 유래한 것으로, 더 이상 쪼갤 수 없는 단일 단위를 가리킨다. 다중 스레드 환경에서는 여러 스레드가 공유 자원을 동시에 접근할 때 데이터 무결성을 보장하기 위해 원자적 연산이 필요하다.
while문 부분을 이렇게도 쓸 수 있다.
while (true)
{
int original = Interlocked.Exchange(ref _locked, 1);
if (original == 0)
break;
}
이 코드에서 original은 스레드의 스택 메모리에 위치한다. 스택 메모리는 각 스레드가 독립적으로 사용하는 메모리 공간이기 때문에, 다른 스레드와 메모리 접근 충돌이 발생하지 않는다.
'공부 > 게임서버' 카테고리의 다른 글
임계영역 (Critical Section) (0) | 2024.11.10 |
---|---|
가시성 문제와 보장 방법 (0) | 2024.11.10 |
컨텍스트 스위칭 (0) | 2024.11.10 |
교착 상태 (Deadlock) (1) | 2024.11.09 |
Interlocked 사용 시 그 반환값을 사용하자 (0) | 2024.11.09 |