이 코드의 결과가 0이 아니라는 놀라운 사실.
namespace ServerCore;
internal class Program
{
private static int number;
private static void Thread1()
{
for (var i = 0; i < 1000000; i++)
number++;
}
private static void Thread2()
{
for (var i = 0; i < 1000000; i++)
number--;
}
private static void Main(string[] args)
{
var t1 = new Task(Thread1);
var t2 = new Task(Thread2);
t1.Start();
t2.Start();
Task.WaitAll(t1, t2);
Console.WriteLine(number);
}
}
결과는 -294605 ??? 왜일까? 10만 번 더하고 10만 번 빼면 0이어야 하는 것 아닌가?
이 코드에는 경합 조건(race condition)이 발생할 가능성이 있다. 경합 조건은 여러 스레드가 동시에 공유 자원에 접근하여 예상치 못한 동작이 일어나는 상황을 말한다. 이 코드의 경우, number
변수에 대한 접근이 Thread1
과 Thread2
에서 동시에 이루어지기 때문에, 두 스레드가 각각 number++
와 number--
연산을 수행할 때 서로 영향을 주어 결과값이 의도하지 않은 값이 될 수 있다.
number++
및 number--
연산은 비원자적 연산이다. 즉, 이 연산들은 여러 단계로 이루어져 있어 실행 중에 다른 스레드가 개입하면 정확한 결과를 보장할 수 없다. 각 연산은 메모리에서 값을 읽고, 연산을 수행한 후 다시 메모리에 쓰는 과정으로 이루어지는데, 그 사이에 다른 스레드가 값을 변경하면 예기치 못한 동작이 발생할 수 있다.
number++와 같은 증감 연산은 C#에서는 단순한 명령어처럼 보이지만, 실제로는 어셈블리 수준에서 여러 단계로 수행된다. 이 과정은 temp 변수와 같은 중간 저장소가 있는 것처럼 동작한다. 일반적인 증감 연산의 단계는 다음과 같다:
- 읽기: 메모리에서 number 값을 레지스터로 가져온다.
- 증감: 레지스터에 있는 값을 1 증가하거나 감소한다.
- 쓰기: 변경된 값을 다시 메모리에 저장한다.
이 과정은 원자적이지 않기 때문에, 여러 스레드가 동시에 number에 접근할 경우 경합 조건이 발생할 수 있다. 예를 들어:
- 스레드 A가 number 값을 읽어 temp에 저장한다.
- 스레드 B도 동시에 number 값을 읽어 temp에 저장한다.
- 스레드 A가 값을 증가시키고 number에 기록한다.
- 스레드 B가 값을 감소시키고 number에 기록한다.
이처럼 두 스레드가 동시에 작업하면 예상하지 못한 최종 값이 남게 될 수 있다. 이를 방지하기 위해 lock 문이나 Interlocked 클래스 같은 동기화 기법을 사용해 원자성을 보장해야 한다.
해결 방법
이 문제를 해결하려면 스레드 간의 동기화를 보장해야 한다. 다음은 몇 가지 해결 방법이다:
lock
문 사용:lock
키워드를 사용하여 임계 구역(critical section)을 설정하여 한 번에 하나의 스레드만 number
에 접근하도록 한다.
private static readonly object _lock = new object();
private static void Thread1()
{
for (var i = 0; i < 1000000; i++)
{
lock (_lock)
{
number++;
}
}
}
private static void Thread2()
{
for (var i = 0; i < 1000000; i++)
{
lock (_lock)
{
number--;
}
}
}
Interlocked
클래스 사용:
private static void Thread1()
{
for (var i = 0; i < 1000000; i++)
{
Interlocked.Increment(ref number);
}
}
private static void Thread2()
{
for (var i = 0; i < 1000000; i++)
{
Interlocked.Decrement(ref number);
}
}
System.Threading.Interlocked
클래스의 메서드를 사용하면 안전하게 증감 연산을 수행할 수 있다.
Interlocked
클래스는 성능 측면에서도 효율적이며 간단하게 경합 조건을 해결할 수 있는 방법이다.
이 두 방법 중 하나를 사용하여 경합 조건을 해결하면 코드의 동기화 문제가 해소되어 예상한 결과를 보장할 수 있다.
'공부 > 게임서버' 카테고리의 다른 글
Monitor 사용 시 주의사항 (0) | 2024.11.13 |
---|---|
Thread.Sleep 과 Thread.Yield (0) | 2024.11.12 |
메모리 영역과 스레드 (0) | 2024.11.11 |
재귀적 lock을 허용하지 않는 ReaderWriterLock 구현 예시 (0) | 2024.11.11 |
임계영역 (Critical Section) (0) | 2024.11.10 |