메모리 베리어
1. 자동 최적화
using System;
using System.Threading.Tasks;
namespace ServerCore
{
class Program
{
static int x = 0;
static int y = 0;
static int r1 = 0;
static int r2 = 0;
static void Thread_1()
{
y = 1; // Store y
r1 = x; // Load x
}
static void Thread_2()
{
x = 1; // Store x
r2 = y; // Load 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}번만에 빠져나옴!");
}
}
}
위의 코드를 분석해보면 무한 루프에 빠져야하는데 실제로 실행해보면 무한 루프를 빠져나오고 매번 실행할 때마다 다른 결과가 나온다. 그 이유는 CPU가 의존성이 없는 명령어라고 판단하면 최적화를 위해 코드의 순서를 자기 멋대로 뒤바꾸기 때문이다.
즉, CPU가 최적화를 위해 코드의 순서를 마음대로 바꾸었고 그 결과 무한루프에 빠져나오는 조건이 충족되어버린 것이다.
싱글쓰레드에서는 이러한 것이 문제가 전혀 되지 않았지만 멀티쓰레드에서는 우리가 예상한 로직과 다른 결과가 나오는 문제가 발생한다.
2. 메모리 베리어
1번에서 나오는 문제들을 해결하기 위해 메모리 베리어를 활용할 수 있다.
메모리 베리어는 두 가지 기능이 있다.
A) 코드 재배치 억제
B) 가시성
코드 재배치 억제
static void Thread_1()
{
y = 1;
Thread.MemoryBarrier(); // 메모리 배리어
r1 = x;
}
static void Thread_2()
{
x = 1;
Thread.MemoryBarrier(); // 메모리 배리어
r2 = y;
}
1번의 코드에서 일부를 가져왔다.
Thread_1에서 y=1;와 r1=x;사이를 메모리 베리어로 막았고,
Thread_2에서 x=1;와 r2=y;사이를 메모리 베리어로 막았다.
이렇게 수정하고 1번의 코드를 다시 실행시켜보면 무한 루프에 빠지는걸 확인할 수 있다. 이러한 결과로 CPU가 최적화를 위해 코드를 마음대로 재배치 하는 걸 막는 것을 확인할 수 있다.
가시성
쓰레드는 캐시에 있는 내용을 메모리에 올리는 작업 혹은 그 반대로 메모리의 내용을 캐시에 올리는 작업인 커밋(Commit)을 수행한다.
그리고 메모리 베리어를 사용하는 부분에서 커밋이 한 번 이루어진다.
즉, 눈에 보이지 않는 내부에서 일어났던 커밋(Commit)을 Thread.MemoryBarrier();라는 코드를 선언하는 곳에서 커밋(Commit)이 이루어진다는 것을 알 수 있게 된다.
참고로, 메모리 베리어는 간접적으로 들어있는 경우가 많다. volatile키워드라던가 lock, 혹은 나중에 배울 원자성(Atomicity)에도 내부적으로 들어있다. 그렇기 때문에 코드를 구현할 때 우리가 직접 사용하는 개념은 아니고 코드 재배치 억제와 커밋에 대해 이해만 해도 좋다.