최적화는 비단 컴파일러만 수행하는 것이 아닌 하드웨어에 의해서도 수행됩니다.
하드웨어가 판단하기에 빠르게 처리할 수 있는 코드의 경우 기존의 순서를 무시하고 우선 처리하게 됩니다,
이는 싱글 스레드에서는 의미가 없지만,
여러 개의 스레드가 각기 다른 작업을 처리하는 멀티 스레드 환경에서는 매우 큰 위험이 됩니다.
아래의 코드를 한번 살펴보겠습니다.
class Program
{
static int x = 0;
static int y = 0;
static int result1 = 0;
static int result2 = 0;
static void Thread_1()
{
y = 1;
result1 = x;
}
static void Thread_2()
{
x = 1;
result2 = y;
}
static void Main(string[] args)
{
int cnt = 0;
while (true)
{
cnt++;
x = y = result1 = result2 = 0;
Task t1 = new Task(Thread_1);
Task t2 = new Task(Thread_2);
t1.Start();
t2.Start();
Task.WaitAll(t1, t2);
if(result1 == 0 && result2 == 0)
break;
}
Console.WriteLine($"Count의 최종 값 : {cnt}");
}
}
위의 코드는 2개의 Task를 생성하고 각각 Thread_1과 Thread_2 함수를 처리합니다.
이때 main 스레드에서는 result1, result2 변수를 통해 무한 반복문을 종료하게 됩니다.
Thread_1과 Thread_2 함수의 구조를 보면 무슨일이 있어도 result1과 result2가 0이 되는 일은 없어보입니다.
하지만 실행결과를 살펴보면 다음과 같이 발생합니다.
무한반복문이 끝나고 로그를 찍은 것을 확인할 수 있습니다.
이는 하드웨어의 최적화에 의해 발생하여 함수 Thread_1과 Thread_2에서의 순서를
하드웨어가 처리하기 쉬운 순서대로 임의로 처리하여 발생합니다.
이 문제를 해결하기 위해 이번에는 최적화를 방지하는 요소 중 하나인 Memory Barrier를 사용하겠습니다.
1) Memory Barrier
Memory Barrier의 용도는 다음의 2개로 구분할 수 있습니다.
코드 재배치를 억제, 가시성 상승의 2개의 용도를 가집니다.
코드 재배치를 억제한다는 것은 말 그대로 임의로 코드의 순서를 바꿔 수행하는 것을 방지합니다.
MemoryBarrier가 호출된 시점에서 데이터에 대한 Store(=저장) / Load(=불러오기)를 전부 막습니다.
이러한 형태를 Full MemoryBarrier라고 합니다.
더욱더 세분화하여 최적화를 방지할수도 있는데,
Load만 막는 것을 Load MemoryBarrier, Store만 막는 것을 Store MemoryBarrier라고 합니다.
가시성의 상승은 메모리에서 데이터를 불러오거나 저장하는 경우에 해당합니다.
MemoryBarrier가 호출된 시점 이후
Store된(=저장된) 데이터는 메모리에 실제로 반영됩니다.
그리고 Load된(=불려와진) 데이터는 실제로 메모리에 반영된 데이터를 가져오게 됩니다.
따라서 MemoryBarrier는
새로운 데이터를 Store하거나, 기존의 데이터를 Load하는 경우에 각각 설정해줘야 합니다.
또한 Memory Barrier는 하나의 스레드에서만 사용해서는 안되고,
반드시 모든 스레드에 사용해 짝을 맞춰줘야 합니다.
그렇지 않을 경우, 사용하지 않은 스레드에서 최적화를 수행하게 됩니다.
그렇다면 이전의 코드를 Memory Barrier를 사용해 수정해보겠습니다.
수정한 코드는 다음과 같습니다.
class Program
{
static int x = 0;
static int y = 0;
static int result1 = 0;
static int result2 = 0;
static void Thread_1()
{
y = 1;
Thread.MemoryBarrier();
result1 = x;
}
static void Thread_2()
{
x = 1;
Thread.MemoryBarrier();
result2 = y;
}
static void Main(string[] args)
{
int cnt = 0;
while (true)
{
cnt++;
x = y = result1 = result2 = 0;
Task t1 = new Task(Thread_1);
Task t2 = new Task(Thread_2);
t1.Start();
t2.Start();
Task.WaitAll(t1, t2);
if(result1 == 0 && result2 == 0)
break;
}
Console.WriteLine($"Count의 최종 값 : {cnt}");
}
}
실행결과를 살펴보면 다음과 같습니다.
최적화가 발생하지 않아 코드의 순서가 바뀌는 일이 없기에
문제없이 무한반복문이 실행중인 것을 확인할 수 있습니다.
'C#' 카테고리의 다른 글
Lock 기초 (1) | 2024.12.02 |
---|---|
Interlocked (0) | 2024.11.27 |
캐싱 (1) | 2024.11.27 |
컴파일러 최적화 (0) | 2024.11.25 |
C#에서 쓰레드를 생성하는 방법 (0) | 2024.11.25 |