C# 멀티스레딩에서 경합 조건과 Interlocked 클래스의 사용
예제 코드 소개
아래의 예제 코드는 두 개의 스레드가 number 변수를 각각 증가 및 감소시키는 방식으로 동작합니다. 하지만, 실행 결과는 예상과 다르게 0이 아닌 다른 값이 출력될 수 있습니다.
class Program
{
// 경합 조건
static int number = 0;
static void Thread_1()
{
for (int i = 0; i < 10000; i++)
number++;
}
static void Thread_2()
{
for (int i = 0; i < 10000; i++)
number--;
}
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(number);
}
}
경합 조건이란?
경합 조건(race condition)이란 여러 스레드가 동시에 공유 자원에 접근할 때 발생할 수 있는 문제입니다. 각 스레드가 자원을 읽고 쓸 때의 순서에 따라 프로그램의 실행 결과가 달라질 수 있습니다.
예제 코드의 문제
위 코드에서 두 스레드 Thread_1과 Thread_2는 각각 number 변수를 10,000번씩 증가 및 감소시킵니다. 이론적으로는 최종 결과가 0이 되어야 하지만, 실행할 때마다 다른 값이 출력될 수 있습니다.
어셈블리 수준에서의 동작
어셈블리 코드에서 number++와 number-- 연산은 세 단계로 나뉩니다:
- 값을 읽는다.
- 값을 수정한다.
- 값을 쓴다.
예를 들어, number++ 연산은 다음과 같이 어셈블리 코드로 번역될 수 있습니다:
mov eax, [number] ; number 값을 레지스터로 이동
add eax, 1 ; 레지스터 값 증가
mov [number], eax ; 증가된 값을 number에 저장
두 스레드가 동시에 number 변수에 접근할 때, 다음과 같은 상황이 발생할 수 있습니다:
- 스레드 1이 number 값을 읽는다.
- 스레드 2가 number 값을 읽는다.
- 스레드 1이 값을 증가시킨다.
- 스레드 2가 값을 감소시킨다.
- 스레드 1이 값을 쓴다.
- 스레드 2가 값을 쓴다.
이 경우, 스레드 1의 연산이 스레드 2의 연산에 의해 덮어쓰여 최종 결과가 예상과 다르게 됩니다.
Interlocked 클래스 사용
Interlocked 클래스는 원자적(atomic) 연산을 제공하여 경합 조건을 방지할 수 있습니다. 원자적 연산은 중간에 다른 스레드가 개입할 수 없도록 보장합니다.
Interlocked.Increment와 Decrement
Interlocked 클래스의 Increment와 Decrement 메서드는 다음과 같이 동작합니다:
- Interlocked.Increment(ref number): number 변수를 원자적으로 증가시킵니다.
- Interlocked.Decrement(ref number): number 변수를 원자적으로 감소시킵니다.
이를 사용하면 경합 조건을 방지할 수 있습니다.
원자성(Atomicity)과 경합 조건
원자성(atomicity)이란 연산이 더 이상 쪼갤 수 없는 하나의 단위로 이루어진다는 의미입니다. 원자적 연산은 중간에 다른 연산이 끼어들 수 없으므로, 여러 스레드가 동시에 실행되더라도 안전하게 공유 자원에 접근할 수 있습니다.
수정된 코드
Interlocked 클래스를 사용하여 경합 조건을 방지한 수정된 코드는 다음과 같습니다:
class Program
{
// 경합 조건
static int number = 0;
static void Thread_1()
{
for (int i = 0; i < 10000; i++)
Interlocked.Increment(ref number);
}
static void Thread_2()
{
for (int i = 0; i < 10000; i++)
Interlocked.Decrement(ref number);
}
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(number);
}
}
이제 두 스레드는 number 변수를 원자적으로 증가 및 감소시킵니다. 따라서 최종 결과는 항상 0이 됩니다.
결론
이번 포스트에서는 C#에서 멀티스레딩 프로그래밍 시 발생할 수 있는 경합 조건 문제와 이를 해결하기 위한 Interlocked 클래스의 사용에 대해 알아보았습니다. 경합 조건은 멀티스레드 환경에서 예상치 못한 결과를 초래할 수 있으며, 이를 방지하기 위해 원자적 연산을 사용하는 것이 중요합니다. Interlocked 클래스의 Increment와 Decrement 메서드를 사용하면 원자성을 보장하여 안전한 멀티스레드 프로그램을 작성할 수 있습니다.
멀티스레딩 프로그래밍은 복잡하지만, 올바른 동기화 기법을 사용하면 안정적이고 효율적인 코드를 작성할 수 있습니다. 추가적인 질문이나 궁금한 점이 있다면 언제든지 댓글로 남겨주세요!
'Programming > C#' 카테고리의 다른 글
C# 멀티스레딩에서 데드락 문제와 해결 방법 (0) | 2024.07.01 |
---|---|
C# 멀티스레딩에서 안전한 공유 자원 관리를 위한 임계영역과 Lock 사용법 (0) | 2024.07.01 |
멀티스레딩에서 발생하는 메모리 재정렬 현상과 해결 방법 (0) | 2024.07.01 |
C# 다차원 배열 순회 행 우선 순회와 열 우선 순회의 성능 차이 (0) | 2024.07.01 |
Visual Studio 닷넷 global using 자동 추가되는 기능 끄기 (0) | 2024.07.01 |