문제

다음 코드 샘플에 대한 질문이 있습니다 (에서 가져온 것 : 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.

라이센스 : CC-BY-SA ~와 함께 속성
제휴하지 않습니다 StackOverflow
scroll top