2009-11-24 4 views
49

일부 언어는 변수를 지원하는 메모리를 읽기 전에 "읽기 메모리 장벽"을 수행하는 것으로 설명 된 volatile 수정자를 제공합니다.읽기 메모리 장벽과 휘발성을 이해하는 방법

일반적으로 읽기 메모리 장벽은 장벽 뒤에 요청 된 읽기를 수행하기 전에 CPU가 장벽보다 먼저 읽기를 수행했는지 확인하는 방법으로 설명됩니다. 그러나이 정의를 사용하면 오래된 값을 읽을 수 있습니다. 다시 말해서, 특정 순서로 읽기를 수행한다고해도, 후속 값 읽기가 실제로 읽기 장벽 시간에 시스템의 최신 내용을 반영하는지 또는 그 후에 연속적으로 쓰여지는지 확인하기 위해 주 메모리 또는 기타 CPU를 참조해야하는 것은 아닙니다. 읽기 장벽.

휘발성은 실제로 최신 값이 읽히지 않았는지 또는 읽혀진 값이 적어도 장벽 앞에서 읽은 값만큼 최신인지를 보증합니까? 아니면 다른 해석? 이 대답의 실용적인 의미는 무엇입니까?

답변

93

읽기 장벽과 쓰기 장벽이 있습니다. 장벽을 확보하고 장벽을 제거하십시오. 그리고 더 많은 것들 (io 대 메모리 등).

장벽은 값의 "최신"값 또는 "신선도"를 제어하지 않습니다. 이들은 메모리 액세스의 상대적 순서를 제어합니다.

쓰기 장벽은 쓰기 순서를 제어합니다. 메모리 쓰기는 느리기 때문에 (CPU 속도와 비교할 때) 일반적으로 쓰기 요청 큐가 있으며 실제로 쓰기 전에 쓰기가 게시됩니다. 대기열에 순서대로 대기 중이지만 대기열 내에있는 동안 쓰기 작업은 재정렬 될 수 있습니다. (어쩌면 '대기열'이 최고의 이름이 아닙니다 ...) 재주문 방지를 위해 쓰기 장벽을 사용하지 않는 한.

판독 장벽은 읽기 순서를 제어합니다. 추측 적 실행 (CPU가 앞서고 메모리에서 일찍로드 됨) 및 쓰기 버퍼가 있기 때문에 (CPU가 메모리가 아닌 쓰기 버퍼의 값을 읽습니다. 즉 CPU가 X = 5, 왜 그것을 다시 읽어, 그냥 아직 가되기를 기다리는 참조하십시오 5 쓰기 버퍼에) 읽기 순서가 발생할 수 있습니다.

컴파일러가 생성 된 코드의 순서와 관련하여 수행하려는 작업과 상관없이 적용됩니다. 즉, C++에서 '휘발성'은 컴파일러에게 "메모리"의 값을 다시 읽도록 코드를 출력하도록 지시하기 때문에 여기서는 도움이되지 않습니다. CPU에서 읽는 방법/위치 (예 : "메모리" CPU 수준에서 많은 것들이 있습니다.)

읽기/쓰기 장애물은 읽기/쓰기 대기열에서 순서가 변경되는 것을 방지하기 위해 블록을 둡니다 (일반적으로 읽기는 대개 큐에 없지만 순서 변경 효과는 동일합니다).

어떤 종류의 블록입니까? - 블록 획득 및/또는 해제.

획득 - 예 : read-acquire (x)는 읽기 대기열에 을 추가하고 대기열을 플러시합니다. (실제로 대기열을 비우지는 않지만이 마커를 다시 읽지 마십시오. , 큐가 플러시 된 것처럼). 그래서 나중에 (코드 순서대로) 읽기가 재 배열 될 수 있지만 x가 읽히기 전에 읽히지는 않습니다.

릴리스 - 예 : write-release (x, 5)는 큐를 플러시 (또는 마커) 한 다음 쓰기 요청을 쓰기 큐에 추가합니다. 따라서 이전의 쓰기는 x = 5 이후에 다시 정렬되지 않지만 나중에 쓰기는 x = 5가되기 전에 다시 정렬 할 수 있습니다.

일반적으로 읽기와 쓰기를 릴리스와 짝을지었습니다. 상이한 조합이 가능하다.

획득 및 릴리스는 순서 변경이 한 방향으로 진행되는 것을 중지하기 때문에 '절반 차단'또는 '절반 차단'으로 간주됩니다.

전체 차단 (또는 전체 차단)은 획득 및 릴리스를 모두 적용합니다 (즉, 재정렬 없음).

일반적으로 lockfree 프로그래밍 또는 C# 또는 java 'volatile'의 경우 원하는 /해야 할 것은 읽기 - 획득 및 쓰기 - 릴리스입니다.

void threadA() 
{ 
    foo->x = 10; 
    foo->y = 11; 
    foo->z = 12; 
    write_release(foo->ready, true); 
    bar = 13; 
} 
void threadB() 
{ 
    w = some_global; 
    ready = read_acquire(foo->ready); 
    if (ready) 
    { 
     q = w * foo->x * foo->y * foo->z; 
    } 
    else 
     calculate_pi(); 
} 

그래서, 우선,이 스레드를 프로그래밍하는 나쁜 방법입니다. 자물쇠가 더 안전합니다. 그러나 장벽을 설명하기 위해 ...

threadA()가 foo를 작성한 후에는 foo-> ready LAST, 정말로 마지막으로 작성해야합니다. 그렇지 않으면 다른 스레드가 foo-> ready early를보고 잘못된 값을 얻을 수 있습니다. x/y/z. 따라서 foo-> ready에 대해 write_release을 사용합니다. 위에서 언급했듯이, 실제로 쓰기 대기열을 플러시 (x, y, z가 확약 된 것을 보장) 한 다음 ready = true 요청을 대기열에 추가합니다. 그런 다음 bar = 13 요청을 추가합니다. 우리가 방금 장벽 (full이 아닌)을 사용 했으므로 준비가되기 전에 작성 될 수 있습니다. 그러나 우리는 상관하지 않는다! 즉, 바는 공유 데이터를 변경하지 않는다고 가정합니다.

이제 threadB()는 '준비'라고 말할 때 준비가 필요하다는 것을 알아야합니다. 그래서 우리는 read_acquire(foo->ready)을합니다. 이 읽기는 읽기 대기열에 추가되고 대기열은 비워집니다. w = some_global도 여전히 대기열에있을 수 있습니다. 따라서 foo-> ready는 전에some_global 앞에 읽을 수 있습니다. 그러나 우리가주의를 기울이고있는 중요한 데이터의 일부가 아니기 때문에 다시는 신경 쓰지 않습니다. 우리가 신경 쓰는 것은 foo-> x/y/z입니다. 따라서 플러시/마커 획득 후 읽기 대기열에 추가되므로 foo-> ready를 읽은 후에 만 ​​읽혀 지도록 보장합니다.

뮤텍스/크리티컬 섹션/등의 잠금 및 잠금 해제에 사용 된 장벽과 일반적으로 동일한 장벽이라는 점에 유의하십시오. (즉, lock()에서 획득, unlock()에서 해제).

그래서,

  • 나는/++ 선택적으로 MS C에 대한 (그리고 C#에서 '휘발성'변수의 기록이 (즉 취득/릴리스) MS의 문서 읽기를위한 일이 무슨 말을 정확히 확신 해요, 하지만 이것은 비표준입니다). 포함 http://msdn.microsoft.com/en-us/library/aa645755(VS.71).aspx를 참조 의미 획득 "한 휘발성 읽기", 나는 자바가 같은 생각

  • "그게은 그 이후에 발생하는 메모리에 대한 참조 이전에 발생하는 보장,이다 ...",하지만 나는 친숙하지 않다. 나는 그것이 정확히 같다고 생각합니다. 왜냐하면 당신은 읽기 - 획득/쓰기 - 릴리스보다 더 많은 보증을 필요로하지 않기 때문입니다.

  • 귀하의 질문에 당신이 올바른 순서에 있다고 생각할 때 그것은 상대적인 순서에 관한 것이라고 생각할 때 - 당신은 단지 순서가 거꾸로되어 있습니다 (즉, "읽은 값은 최소한 장벽 앞에서 읽 힙니다. "- 아니요, 장벽이 중요하지 않기 전에 읽습니다. 장벽 뒤에서 읽기가 완료되면 AFTER가 보장되고 쓰기는 반대가됩니다.)

  • 언급 한 바와 같이 순서 재 지정은 읽기 및 쓰기 모두에서 발생하므로 한 스레드에서만 장벽을 사용하고 다른 스레드에서는 장벽을 사용하지 않습니다. 즉, 쓰기 - 릴리스는 읽기 - 취득이 충분하지 않습니다. 즉, 올바른 순서로 쓰더라도 쓰기 장벽을 사용하기 위해 읽기 장벽을 사용하지 않으면 잘못된 순서로 읽을 수 있습니다.

  • 그리고 마지막으로 lock-free 프로그래밍과 CPU 메모리 아키텍처는 실제로 그보다 훨씬 복잡 할 수 있지만 획득/릴리스를 고수하면 상당히 멀리 할 수 ​​있습니다.

+1

write_release와 read_acquire가 동일한 준비 변수를 참조하는 것이 중요합니까? 또는 둘 다 별도의 덤미 변수를 사용할 수 있습니까? 전달 된 가치는 목적이없는 것 같습니다. –

+2

동기화하려는 스레드에 대해 동일한 변수를 사용하는 것이 중요합니다. 마치 동일한 스레드를 사용하거나 일반적인 스레딩에서 잠글 필요가있는 것처럼. 내 threadA/B 예제에서 우리는 foo-> x, y, z가 foo-> ready보다 먼저 작성되도록하고 싶습니다. 그렇지 않으면 foo가 실제로 준비되기 전에 누군가 'ready == true'를 볼 수 있습니다. READY 쪽에서는 준비가되기 전에 x, y, if (foo-> ready) '. 장벽이 다른 더미 변수에 있다면 동기 점이 없습니다. – tony

8

volatile 대부분의 프로그래밍 언어는 실제 CPU 읽기 메모리 장벽을 의미하지 않지만 컴파일러가 레지스터에서 캐싱을 통해 읽기를 최적화하지 않도록 지시합니다. 이것은 읽기 프로세스/쓰레드가 "결국"값을 얻는다는 것을 의미합니다. 일반적인 기술은 신호 처리기에 부울 volatile 플래그를 설정하고 주 프로그램 루프에서 확인하는 것입니다.

대조적으로 CPU 메모리 장벽은 CPU 명령어를 통해 직접 제공되거나 특정 어셈블러 니모닉 (예 : x86의 경우 lock 접두어)과 함 께 제공되며 메모리 매핑 IO에 대한 읽기 및 쓰기 순서가있는 하드웨어 장치와 통신 할 때 사용됩니다 멀티 프로세싱 환경에서 레지스터가 중요하거나 메모리 액세스를 동기화하는 것입니다.

질문에 대답하려면 - 아니요, 메모리 장벽은 "최신"값을 보장하지 않지만 메모리 액세스 연산을 보장합니다. 이것은 lock-free 프로그래밍에서 매우 중요합니다.

Here은 CPU 메모리 장벽의 뇌관 중 하나입니다.

+0

저는 C 및 C++의 많은 구현에서이 사실을 알고 있습니다. 제 질문은 Java 및 .NET과 같은 가상 시스템 플랫폼과 가장 관련이 있습니다. –

+0

Java 및 C#과 같은 VM 기반 언어의 경우 "메모리 모델"이 무엇인지 알아야합니다. –

+0

'volatile'이 충분하지 않다는 것을 명심하십시오. 시그널 핸들러에서 표준 호환을 위해'volatile sig_atomic_t'를 사용해야합니다. – Jed

관련 문제