멀티스레딩에서 발생하는 메모리 재정렬 현상과 해결 방법
예제 코드 소개
아래의 예제 코드는 두 개의 스레드가 각각 x와 y 변수를 설정하고, 다른 변수인 r1과 r2에 값을 할당하는 방식으로 동작합니다. 코드를 여러 번 실행해 보면, r1과 r2 값이 동시에 0이 되는 경우가 발생할 수 있습니다.
class Program
{
static int x = 0;
static int y = 0;
static int r1 = 0;
static int r2 = 0;
static void Thread_1()
{
y = 1;
r1 = x;
}
static void Thread_2()
{
x = 1;
r2 = y;
}
static void Main(string[] args)
{
int count = 0;
while (true)
{
count++;
x = y = r1 = r2 = 0;
Task t1 = new Task(Thread_1);
Task t2 = new Task(Thread_2);
t1.Start();
t2.Start();
Task.WaitAll(t1, t2);
if (r1 == 0 && r2 == 0)
break;
}
Console.WriteLine($"{count}번만에 빠져나옴!");
}
}
실행 횟수는 컴퓨터의 성능마다 크게 차이날 수 있습니다.
문제 설명: 메모리 재정렬 (Memory Reordering)
멀티스레드 환경에서 CPU는 성능 최적화를 위해 명령어 실행 순서를 변경할 수 있습니다. 이로 인해 코드에서 예상한 순서와 실제 실행 순서가 달라질 수 있습니다. 이를 메모리 재정렬이라고 합니다. 메모리 재정렬은 특히 멀티코어 프로세서에서 빈번하게 발생하며, 이는 메모리 일관성 모델(memory consistency model)에서 허용하는 행동입니다.
예제 코드의 문제
코드에서 두 스레드는 다음과 같은 순서로 명령을 실행할 수 있습니다:
- Thread_1이 y = 1을 실행
- Thread_2가 x = 1을 실행
- Thread_1이 r1 = x를 실행 (이 시점에서 x는 아직 0)
- Thread_2가 r2 = y를 실행 (이 시점에서 y는 아직 0)
이 경우 r1과 r2는 모두 0이 됩니다. 이는 두 스레드의 명령어 실행 순서가 서로 엇갈려서 발생합니다.
해결 방법: 메모리 배리어와 동기화
멀티스레드 환경에서 메모리 재정렬 문제를 해결하기 위해 메모리 배리어(memory barrier) 또는 동기화 기법을 사용할 수 있습니다. C#에서는 volatile 키워드와 lock 구문, 그리고 다양한 동기화 프리미티브를 제공하여 메모리 재정렬을 방지할 수 있습니다.
1. volatile 키워드 사용
volatile 키워드는 변수의 값이 여러 스레드에 의해 변경될 수 있음을 컴파일러와 런타임에 알려줍니다. 이는 해당 변수에 대한 모든 읽기 및 쓰기 작업이 메모리 배리어를 통해 순서대로 수행되도록 합니다.
static volatile int x = 0;
static volatile int y = 0;
2. lock 구문 사용
lock 구문을 사용하여 코드 블록에 대한 배타적 접근을 보장할 수 있습니다. 이는 특정 코드 블록 내에서 한 번에 하나의 스레드만 실행되도록 합니다.
static readonly object _lock = new object();
static void Thread_1()
{
lock (_lock)
{
y = 1;
r1 = x;
}
}
static void Thread_2()
{
lock (_lock)
{
x = 1;
r2 = y;
}
}
3. Interlocked 클래스 사용
Interlocked 클래스는 원자적(atomic) 연산을 제공합니다. 이는 변수의 값을 읽고 쓰는 작업을 하나의 불가분한 연산으로 처리합니다.
static void Thread_1()
{
Interlocked.Exchange(ref y, 1);
r1 = Interlocked.CompareExchange(ref x, 0, 0);
}
static void Thread_2()
{
Interlocked.Exchange(ref x, 1);
r2 = Interlocked.CompareExchange(ref y, 0, 0);
}
3. MemoryBarrier 사용
static void Thread_1()
{
y = 1;
Thread.MemoryBarrier();
r1 = x;
}
static void Thread_2()
{
x = 1;
Thread.MemoryBarrier();
r2 = y;
}
CPU나 컴파일러에게 barrier 명령문 전 후의 메모리 연산을 순서에 맞게 실행하도록 강제하는 기능이다. 즉 barrier 이전에 나온 연산들이 barrier 이후에 나온 연산보다 먼저 실행이 되는게 보장되어야 하는 것이다.
결론
이번 포스트에서는 멀티스레딩 환경에서 발생할 수 있는 메모리 재정렬 문제와 이를 해결하기 위한 방법에 대해 알아보았습니다. 메모리 재정렬은 CPU의 성능 최적화를 위한 과정에서 발생하며, 멀티스레드 프로그램에서 예상치 못한 결과를 초래할 수 있습니다. 이를 방지하기 위해 volatile 키워드, lock 구문, Interlocked 클래스와 같은 동기화 기법을 사용하여 코드의 실행 순서를 보장해야 합니다.
멀티스레딩 프로그래밍은 복잡하지만, 올바른 동기화 기법을 사용하면 안정적이고 효율적인 코드를 작성할 수 있습니다. 추가적인 질문이나 궁금한 점이 있다면 언제든지 댓글로 남겨주세요!
'Programming > C#' 카테고리의 다른 글
C# 멀티스레딩에서 안전한 공유 자원 관리를 위한 임계영역과 Lock 사용법 (0) | 2024.07.01 |
---|---|
C# 멀티스레딩에서 경합 조건과 Interlocked 클래스의 사용 (0) | 2024.07.01 |
C# 다차원 배열 순회 행 우선 순회와 열 우선 순회의 성능 차이 (0) | 2024.07.01 |
Visual Studio 닷넷 global using 자동 추가되는 기능 끄기 (0) | 2024.07.01 |
C# 스레드 동기화 문제 해결하기 디버그 모드와 릴리즈 모드에서의 차이점 (0) | 2024.07.01 |