메모리 펜스는 데이터의 "신선도"에 어떤 영향을 미칩니 까?
-
20-09-2019 - |
문제
다음 코드 샘플에 대한 질문이 있습니다 (에서 가져온 것 : http://www.albahari.com/threading/part4.aspx#_nonblockingsynch)
class Foo
{
int _answer;
bool _complete;
void A()
{
_answer = 123;
Thread.MemoryBarrier(); // Barrier 1
_complete = true;
Thread.MemoryBarrier(); // Barrier 2
}
void B()
{
Thread.MemoryBarrier(); // Barrier 3
if (_complete)
{
Thread.MemoryBarrier(); // Barrier 4
Console.WriteLine (_answer);
}
}
}
다음은 다음과 같이 설명합니다.
"장벽 1과 4는이 예제가“0”을 쓰지 못하게합니다. 장벽 2와 3은 신선도 보증을 제공합니다. 그들은 b가 a 후에 실행되면 _complete를 읽는 것이 참으로 평가 될 것입니다. "
메모리 장벽을 사용하는 것이 명령어 재정의 영향에 어떤 영향을 미치는지 이해하지만 이것이 무엇입니까? "신선도 구라 란티" 언급 되었습니까?
이 기사의 뒷부분에서 다음 예제도 사용됩니다.
static void Main()
{
bool complete = false;
var t = new Thread (() =>
{
bool toggle = false;
while (!complete)
{
toggle = !toggle;
// adding a call to Thread.MemoryBarrier() here fixes the problem
}
});
t.Start();
Thread.Sleep (1000);
complete = true;
t.Join(); // Blocks indefinitely
}
이 예제는 다음과 같이 설명합니다.
"완전한 변수가 CPU 레지스터에 캐시되기 때문에이 프로그램은 결코 종료되지 않습니다. thread.
다시 ... 여기서 무슨 일이야?
해결책
첫 번째 경우에는 장벽 1이 보장합니다 _answer
전에 작성되었습니다 _complete
. 코드 작성 방법 또는 컴파일러 또는 CLR이 CPU를 지시하는 방법에 관계없이 메모리 버스 읽기/쓰기 대기열 요청을 재정렬 할 수 있습니다. 장벽은 기본적으로 "계속되기 전에 대기열을 플러시"라고 말합니다. 마찬가지로 Barrier 4는 확인합니다 _answer
후 읽습니다 _complete
. 그렇지 않으면 CPU2는 물건을 재정렬하고 오래된 것을 볼 수 있습니다 _answer
"새로운" _complete
.
장벽 2와 3은 어떤 의미에서는 쓸모가 없습니다. 설명에는 "After": IE "라는 단어가 포함되어 있습니다. B가 A 이후에 실행되는 것은 무엇을 의미합니까? B와 A가 동일한 CPU에 있으면 B는 후에 B가 될 수 있습니다. 그러나이 경우 동일한 CPU는 메모리 장벽 문제가 없음을 의미합니다.
따라서 B와 A가 다른 CPU에서 실행하는 것을 고려하십시오. 이제 아인슈타인의 상대성 이론과 마찬가지로 다른 위치/CPU에서 시간을 비교하는 개념은 실제로 의미가 없습니다. 그것에 대해 생각하는 또 다른 방법 - b 이후에 B가 실행되었는지 알 수있는 코드를 쓸 수 있습니까? 그렇다면 메모리 장벽을 사용하여 그렇게했습니다. 그렇지 않으면 말할 수 없으며 묻는 것이 합리적이지 않습니다. 또한 하이젠 부르크의 원리와 유사합니다. 관찰 할 수 있다면 실험을 수정했습니다.
그러나 물리학을 제쳐두고 기계의 후드를 열 수 있다고 가정 해 봅시다. 보다 실제로 기억 위치 _complete
사실이었다 (A가 실행 되었기 때문에). 이제 배리어 3없이 B를 실행합니다. CPU2는 여전히 보이지 않을 수 있습니다. _complete
사실. 즉 "신선한"것이 아닙니다.
하지만 아마도 기계를 열고 볼 수 없을 것입니다. _complete
. CPU2에서 발견 한 결과를 B에 전달하지도 않습니다. 당신의 유일한 의사 소통은 CPU 자체가하는 일입니다. 따라서 장벽없이 전/후에 결정할 수 없다면 "장벽없이 A 이후에 실행되는 경우 B에게 어떻게 발생하는지"묻습니다. 의미가 없습니다.
그건 그렇고, C #에서 사용할 수있는 것이 확실하지 않지만 일반적으로 수행되는 작업 및 코드 샘플 # 1에 실제로 필요한 것은 쓰기시 단일 릴리스 장벽이며 읽기에 대한 단일 획득 장벽입니다.
void A()
{
_answer = 123;
WriteWithReleaseBarrier(_complete, true); // "publish" values
}
void B()
{
if (ReadWithAcquire(_complete)) // subscribe
{
Console.WriteLine (_answer);
}
}
"구독"이라는 단어는 종종 상황을 설명하는 데 사용되지 않지만 "게시"는입니다. 스레딩에 관한 Herb Sutter의 기사를 읽는 것이 좋습니다.
이것은 장벽을 넣습니다 바로 그거죠 올바른 장소.
코드 샘플 #2의 경우 실제로 메모리 장벽 문제가 아니며 컴파일러 최적화 문제입니다. complete
레지스터에서. 메모리 장벽은 volatile
, 그러나 외부 기능을 호출 할 것입니다 - 컴파일러가 해당 외부 기능이 수정되었는지 여부를 알 수없는 경우 complete
그렇지 않으면 메모리에서 다시 읽을 것입니다. 즉 주소를 전달할 수 있습니다 complete
어떤 기능 (컴파일러가 세부 사항을 검사 할 수없는 곳에 정의 됨) : :
while (!complete)
{
some_external_function(&complete);
}
함수가 수정되지 않더라도 complete
, 컴파일러가 확실하지 않으면 레지스터를 다시로드해야합니다.
즉 코드 1과 코드 2의 차이점은 코드 1이 A와 B가 별도의 스레드에서 실행될 때만 문제가 있다는 것입니다. 코드 2는 단일 스레드 머신에서도 문제가있을 수 있습니다.
실제로 다른 질문은 - 컴파일러가 While 루프를 완전히 제거 할 수 있습니까? 생각한다면 complete
다른 코드는 도달 할 수 없습니까? 왜 그렇지 않습니까? 즉, 이동하기로 결정한 경우 complete
레지스터로 루프를 완전히 제거 할 수도 있습니다.
편집 : OPC의 의견에 답하기 위해 (내 대답은 너무 큽니다. 주석 블록이 너무 큽니다) :
Barrier 3은 CPU가 보류중인 읽기 (및 쓰기) 요청을 플러시하도록 강요합니다.
_complete를 읽기 전에 다른 읽기가 있는지 상상해보십시오.
void B {}
{
int x = a * b + c * d; // read a,b,c,d
Thread.MemoryBarrier(); // Barrier 3
if (_complete)
...
장벽이 없으면 CPU는이 5 개의 읽기 요청 '보류 중'을 모두 가질 수 있습니다.
a,b,c,d,_complete
장벽이 없으면 프로세서가 이러한 요청을 다시 주문하여 메모리 액세스를 최적화 할 수 있습니다 (예 : _complete 및 'a'가 동일한 캐시 라인에있는 경우).
장벽을 사용하면 CPU는 _complete가 요청으로 표시되기 전에 메모리에서 A, B, C, D를 가져옵니다. 'B'(예 : 'B'가 _complete 전에 읽어보십시오 - 즉 재주문 없음.
문제는 - 어떤 차이가 있습니까?
a, b, c, d가 _complete와 독립적이면 중요하지 않습니다. 모든 장벽은 느리게하는 것입니다. 그래, _complete
읽습니다 나중에. 그래서 데이터는입니다 신입생. 읽기 전에 잠을 자거나 (100) 또는 바쁜 옷을 입는다. :-)
요점은 - 상대적으로 유지하는 것입니다. 다른 데이터와 관련하여 데이터를 읽거나 작성해야합니까? 그게 질문입니다.
그리고 기사의 저자를 내려 놓지 않기 위해 - 그는 "B가 A ... 이후에 달렸다면"를 언급합니다. 그가 A 후 B가 코드에 중요하거나, 코드에 의해 관찰 될 수 있거나, 중요하지 않다는 것을 상상하고 있는지는 정확히 명확하지 않습니다.
다른 팁
코드 샘플 #1 :
각 프로세서 코어에는 메모리의 일부 사본이있는 캐시가 포함되어 있습니다. 캐시를 업데이트하는 데 약간의 시간이 걸릴 수 있습니다. 메모리 장벽은 캐시가 메인 메모리와 동기화되도록 보장합니다. 예를 들어, 여기서 장벽 2와 3이없는 경우이 상황을 고려하십시오.
프로세서 1은 a ()를 실행합니다. _complete의 새로운 값을 캐시에 씁니다 (아직 메인 메모리에 반드시는 아닙니다).
프로세서 2는 B ()를 실행합니다. _complete의 값을 읽습니다. 이 값이 이전에 캐시에 있으면 신선하지 않을 수 있으므로 (즉, 기본 메모리와 동기화되지 않음) 업데이트 된 값을 얻지 못할 수 있습니다.
코드 샘플 #2 :
일반적으로 변수는 메모리에 저장됩니다. 그러나 단일 함수로 값이 여러 번 읽히는다고 가정합니다. 최적화로 컴파일러는 CPU 레지스터로 한 번 읽은 다음 필요할 때마다 레지스터에 액세스하기로 결정할 수 있습니다. 이것은 훨씬 빠르지 만 기능이 다른 스레드에서 변수의 변경 사항을 감지하는 것을 방지합니다.
여기서 메모리 장벽은 기능을 메모리에서 변수 값을 다시 읽도록 강요합니다.
Calling Thread.MemoryBarrier ()는 변수의 실제 값으로 레지스터 캐시를 즉시 새로 고칩니다.
첫 번째 예에서 "신선함" _complete
설정 직후와 사용 직전에 메소드를 호출하여 제공됩니다. 두 번째 예에서는 초기입니다 false
변수의 값 complete
스레드의 자체 공간에 캐시되며 실행 된 스레드의 "내부"에서 실제 "외부"값을 즉시 보려면 다시 동기화해야합니다.
"신선함"보증은 단순히 장벽 2와 3의 값을 강요한다는 것을 의미합니다. _complete
메모리에 기록 될 때마다 가능한 한 빨리 볼 수 있습니다.
장벽 1과 4는 일관성 관점에서 실제로 불필요합니다. answer
읽은 후 읽습니다 complete
.