지난 시간에 Release 시에 최적화를 자동으로 해주어 오류를 만들던 거를 'volatile' 키워드로 예외처리를 해주는 방법을 사용했는데 이번 메모리 배리어의 경우도 유사하다고 볼 수 있다.
실습을 통해 빠르게 배우는게 이해도 잘 되고 좋은 거 같다. 얼마 후 있을 롤 MSI에서 T1이 우승하길 바라며 코드를 작성했다.
namespace ServerCore
{
class Program
{
static int x = 0;
static int y = 0;
static int r1 = 0;
static int r2 = 0;
static void T1()
{
y = 1; // Store
r1 = x; // Load
}
static void RNG()
{
x = 1; // Store
r2 = y; // Load
}
static void Main(string[] args)
{
int count = 0;
while (true)
{
count++;
x = y = r1 = r2 = 0;
Task t1Win = new Task(T1);
Task t1Win2 = new Task(RNG);
t1Win.Start();
t1Win2.Start();
Task.WaitAll(t1Win, t1Win2);
if (r1 == 0 && r2 == 0)
break;
}
Console.WriteLine("몇 번만에 성공임? " + count);
}
}
}
먼저 위와 같은 코드가 있다고 하자. 코드의 구조를 봤을 때 x, y에 1이 대입하고 그걸 r1, r2에 대입하는 구조로 되어있는데 이렇게 하면 while loop를 빠져나올 수 있을까?
응..?
어떻게 r1, r2가 0이 되는 경우가 생긴걸까? 정답은 역시 최적화에 있었다
T1 함수에서 y =1 라인과 r1 =x 라인이 서로 상관관계가 없기 때문에 내부적으로 판단했을 때 더 좋은 효율을 만들기 위해 순서를 바꾸는 경우가 생긴다.
그래서 x = 0, y = 0 일 때 r1 = x, r2 = y가 되며 두 변수의 값이 0이 되는 상황이 발생한다.
최적화 해주는건 고맙지만, 이런 식으로 나도 모르게 바꿔버리면 곤란해지는 경우가 생길 수 있다. 하지만 컴퓨터는 이런 내 마음을 모르기에 정확하게 알려주어야 한다.
먼저 메모리 베리어에는 두 가지 특성이 있다.
위에서 처럼 코드를 맘대로 재배치하여 원하지 않는 결과 도출을 방지해줄 수 있다.
이렇게 순서를 맘대로 바꾸던 곳에 Thread.MemoryBarrier()라는 바리게이트를 세워 서로 순서가 바뀔 수 없게 제한을 해준다.
메모리 베리어를 사용하면 예상하던대로 while loop에 갇히게 된다.
멀티 쓰레드에서는 서로간에 캐시에만 저장하고 메모리에 공유하지 않아 정보의 불균형이 발생할 수 있다고 저번 캐시 이론에서 공부했었다.
이떄 메모리 베리어를 사용할 경우 캐시에만 등록 되어있던거를 메모리에 올려주거나(Store), 메모리에 저장 된 최신 상태의 값을 가져오는(Load) 역할을 해준다.
유명한 예제중에 하나이며 이걸 이해하면 오늘 배운걸 이해할 수 있다고 한다. 왜 계속 Thread.MemoryBarrier() 하는걸 까? 하지만 주석을 통해 이해하자면
이렇게 Store와 Load를 계속 최신화 해주어 가시성을 높이는 작업을 하기 위해 지속적으로 사용되고 있다. 이러면 원치 않는 결과가 나오는 것을 방지 할 수 있을 것이다.