class Program
{
static bool stop = false;
public static void Main(string[] args)
{
var t = new Thread(() =>
{
Console.WriteLine("thread begin");
bool toggle = false;
while (!stop)
{
toggle = !toggle;
}
Console.WriteLine("thread end");
});
t.Start();
Thread.Sleep(1000);
stop = true;
Console.WriteLine("stop = true");
Console.WriteLine("waiting...");
t.Join();
}
}
위 코드에서 while 안의 구문을 풀어서 쓰면 아래와 같이 표현할 수 있다.
if (stop) break;
toggle = !toggle;
if (stop) break;
toggle = !toggle;
if (stop) break;
toggle = !toggle;
/* 반복... */
현재의 코드에서는 어떠한 펜스, 또는 volatile
이 없기 때문에 컴파일러는 최적화를 위해서 마음대로 stop
을 읽어오는 순서를 변경할 수 있다.
(컴파일러는 싱글 스레드 모델로 최적화를 수행함을 기억)
암튼 이렇게 재배치될 수 있음
if (stop) break;
if (stop) break;
if (stop) break;
toggle = !toggle;
toggle = !toggle;
toggle = !toggle;
이렇게 변환된 코드는 CLR Memory Model Rule(의 6번) 에 의해서 삭제될 수도 있다.
Rule 6: Loads and stores may only be deleted when coalescing adjacent loads and stores from/to the same location.
따라서 다시 이렇게 변경되는것도 가능하다.
(toggle = !toggle
이 같은 원리로 압축되지 못하는 이유는, 저건 load-store 라서)
if (stop) break;
toggle = !toggle;
toggle = !toggle;
toggle = !toggle;
컴파일러는 싱글 스레드를 바라보기 때문에 처음의 코드와, 마지막의 압축된 코드는 그냥 동일하게 취급된다. 어차피 컴파일러가 바라보는 스레드에서는 stop에 store하는놈이 한개도 없기때문이다. 실제로 실행결과가 달라지던 말던..
이러한 현상을 막는건 대충 두가지 방법이 있는데,
-
volatile
volatile은 최적화 하지 마라 라는 의미로 널리 알려져 있다. 어떠한 필드를 volatile로 설정하면 컴파일러는 해당 값에 대해서싱글 스레드를 바라보는 최적화(??)
를 하지 않는다. 아무튼 해당 변수가 다른 스레드에 의해 값이 변경될 수 있음을 가정하고 동작하게 되기 때문에 위와 같은 압축을 시키지 않는다. -
MemoryBarrier
메모리 배리어는 재배치 금지(또는 실행 순서 보장)를 알리는 역할을 한다.while
안에 메모리 배리어를 넣으면if (stop)
을 재배치할 수 없기 때문에 생략하는것도 불가능하게 된다.
결국은 재배치에 의한 최적화에 의해 발생한 문제임으로, 재배치를 못하게 하면 된다.
차이점이 있다면 volatile
은 변수에 대해서, MemoryBarrier
는 코드 영역에 대해서 라는 정도의 차이점이 존재한다.
아니면 __lock__을 사용하자, 락은 메모리 배리어를 생성한다.
- .Net은 Full-Fence (Thread.MemoryBarrier 단일 API) 만을 제공한다. (어차피 x86이라..)
- 설명을 위해서 인과 관계를 바꿨는데, 재배치 했는데 어쩌다보니 주르륵 연속된 load 가 발생해서 얘를 지웟다! 기 보다는, 저걸 그냥 지워버리기 위해서 재배치를 한것이 맞겠지. (가 컴파일러를 안짜서 모름)
- volatile에 대해서는 그냥 최적화 하지 마라로 퉁쳐놨지만, 원문 링크의 답변 중
Delayed writing
에 더 자세히 설명되어 있다. 사실 저설명이 맞음.