2016-10-10 2 views
3

최근 샘플 코드가 있습니다 (실제 코드는 훨씬 복잡합니다). 한스 보엠 (Hans Boehm)의 cppcon16이 원자 적으로 얘기하는 것을 본 후, 나는 코드가 작동하는지 조금 걱정된다.원자 단위를 사용하여 단일 프리 프로듀서 다중 소비자 데이터 구조체를 잠급니다.

produce은 단일 제작자 스레드에 의해 호출되고 consume은 다중 소비자 스레드에 의해 호출됩니다. 생산자는 2, 4, 6, 8, ...과 같은 순차 번호의 데이터 만 업데이트하지만 데이터가 더럽다는 것을 나타 내기 위해 데이터를 업데이트하기 전에 1, 3, 5, 7 등과 같은 홀수 시퀀스 번호로 설정합니다. 소비자는 동일한 순서 (2, 4, 6, ...)로 데이터를 가져 오려고합니다.

소비자는 데이터가 양호한 지 확인하기 위해 읽은 다음 시퀀스 num을 확인합니다 (읽는 동안 생산자가 업데이트하지 않음).

x86_64가 다른 상점의 상점을 다시 정렬하지 않거나 상점이나로드로 인해로드되지 않기 때문에 내 코드가 x86_64 (내 대상 플랫폼)에서 잘 작동한다고 생각하지만 다른 플랫폼에서는 잘못된 것 같습니다.

소비자가 손상된 데이터를 읽지 만 여전히 t == t2이 성공할 수 있도록 데이터 생성 (생산 중)을 위의 '저장 (n-1)'로 이동할 수 있다고 수정합니까? 86을 대상으로 할 때 컴파일러는 C++ 추상 기계,되지 않은 강력한 아키텍처에 의존하는 행동 프로그램의 동작을 유지하기 위해 최적화 때문에

struct S 
{ 
    atomic<int64_t> seq; 
    // data members of primitive type int, double etc  
    ... 
}; 

S s; 

void produce(int64_t n, ...) // ... for above data members 
{ 
    s.seq.store(n-1, std::memory_order_release); // indicates it's working on data members 

    // assign data members of s 
    ... 

    s.seq.store(n, std::memory_order_release); // complete updating 
} 

bool consume(int64_t n, ...) // ... for interested fields passed as reference 
{ 
    auto t = s.load(std::memory_order_acquire); 

    if (t == n) 
    { 
     // read fields 
     ... 

     auto t2 = s.load(std::memory_order_acquire); 
     if (t == t2) 
      return true; 
    }   

    return false; 
} 
+0

주의 사항 : 이와 같은 잠금 장치가없는 구조는 올바르게 구현하기가 어렵습니다. 많은 함정이 있습니다. http://blog.memsql.com/common-pitfalls-in-writing-lock-free-algorithms/ –

+0

http://en.cppreference.com/w/cpp/atomic/memory_order#Release - Aququire_ordering은이 질문을 정확하게 처리합니다. 그것은 당신을 위해 그것을 대답합니까? –

+0

마이클, cppref 링크는 제 질문과 매우 다릅니다. 내가 해결하려고하는 문제는 제작자가 동일한 'S'를 업데이트한다는 것입니다. 여러 번, 나는 생산자가 더 높은 seq no로 데이터를 업데이트하지 않으면 소비자가 지정된 시퀀스 번호의 데이터를 읽도록하고 싶다. – Derek

답변

3

Compile-time reordering은 여전히 ​​당신을 물지 수 있습니다. memory_order_seq_cst을 피하기 위해 순서를 변경할 수 있습니다.

예, 귀하가 제안한대로 상점을 재정렬 할 수 있습니다. an acquire-load is only a one-way barrier 이후로드는 t2로드로 재주문 할 수 있습니다. 컴파일러가 t2 검사를 완전히 최적화하는 것은 합법적입니다. 재정렬이 가능하면 컴파일러는 이 항상 일 것이라고 판단하고 더 효율적인 코드를 작성하기 위해 as-if 규칙을 적용합니다. (.. 현재 컴파일러는 일반적으로하지 않습니다, 그러나 이것은 확실히 작성된 현재 표준에 의해 허용되는 the conclusion of a discussion about this, with links to standards proposals 참조) 재정렬을 방지하기위한

옵션은 다음과 같습니다

  • 모든 데이터 가맹점/부하를 확인 해제와 함께 원자력과 의미를 습득한다. (마지막 데이터 멤버의 획득로드는 t2로드가 먼저 수행되도록합니다.)
  • 을 사용하면 모든 비 원자력로드 및 비 원자력로드를 그룹으로 정렬 할 수 있습니다.

    Jeff Preshing이 설명한대로 a mo_releasefence isn't the same thing as a mo_release store은 우리가 필요로하는 양방향 장벽입니다. std :: atomic은 울타리에 다른 이름을 부여하는 대신 std :: mo_ names를 재활용합니다.

    (비 원자력 매장 /로드는 기술적으로 정의되지 않은 동작이므로 보지 않기로 결정하더라도 다시 작성되는 동안 읽는 것이므로 mo_relaxed과 함께 원자 적이어야합니다. 당신이 읽은 것.추가 컴파일러 장벽


    void produce(int64_t n, ...) // ... for above data members 
    { 
        /*********** changed lines ************/ 
        std::atomic_signal_fence(std::memory_order_release); // compiler-barrier to make sure the compiler does the seq store as late as possible (to give the reader more time with it valid). 
        s.seq.store(n-1, std::memory_order_relaxed);   // changed from release 
        std::atomic_thread_fence(std::memory_order_release); // StoreStore barrier prevents reordering of the above store with any below stores. (It's also a LoadStore barrier) 
        /*********** end of changes ***********/ 
    
        // assign data members of s 
        ... 
    
        // release semantics prevent any preceding stores from being delayed past here 
        s.seq.store(n, std::memory_order_release); // complete updating 
    } 
    
    
    
    bool consume(int64_t n, ...) // ... for interested fields passed as reference 
    { 
        if (n == s.seq.load(std::memory_order_acquire)) 
        { 
         // acquire semantics prevent any reordering with following loads 
    
         // read fields 
         ... 
    
        /*********** changed lines ************/ 
         std::atomic_thread_fence(std::memory_order_acquire); // LoadLoad barrier (and LoadStore) 
         auto t2 = s.seq.load(std::memory_order_relaxed); // relaxed: it's ordered by the fence and doesn't need anything extra 
         // std::atomic_signal_fence(std::memory_order_acquire); // compiler barrier: probably not useful on the load side. 
        /*********** end of changes ***********/ 
         if (n == t2) 
          return true; 
        } 
    
        return false; 
    } 
    

    공지 사항)

은 (signal_fence는 컴파일시 컴파일러는 처음으로 하나 개의 반복에서 두 번째 가게를 병합하지 않도록하기 위해 재정렬)에 영향을 루프에서 실행되는 경우 다음 반복에서 저장합니다. 또는 더 일반적으로 영역을 무효화하는 저장소가 가능한 한 늦게 수행되어 오탐 (false positive)을 줄 이도록하십시오. (아마도 실제 컴파일러에서는 필요하지 않을 것이고,이 함수를 호출하는 사이에 많은 코드가있을 것입니다.하지만 signal_fence는 어떤 명령어로도 컴파일되지 않으며, 첫 번째 저장소를 mo_release으로 유지하는 것보다 더 나은 선택입니다. 릴리스 저장소를 저장하는 아키텍처 -fence를 추가 지침에 따라 컴파일하면 이완 된 저장소에서 두 개의 별도 장벽 지침을 사용하지 않아도됩니다.)

이전 저장소에서 릴리스 저장소를 사용하여 첫 번째 저장소를 다시 정렬 할 가능성에 대해 걱정했습니다. 그러나 나는 두 상점이 같은 주소에 있기 때문에 일어날 수 있다고 생각하지 않습니다. (컴파일 타임에 표준은 적대적인 컴파일러가 그것을 가능하게 할 수도 있지만, 정상적인 컴파일러는 다른 컴파일러를 넘겨 줄 수 있다고 생각한다면 상점을 전혀하지 않을 것입니다.) 약한 컴파일러에서 실행될 때, 순서가 지정된 아키텍처에서 동일한 주소의 상점이 전 세계적으로 보이지 않게 될지 확실하지 않습니다. 생산자가 연속적으로 호출되지 않기 때문에 이것은 실제 생활에서 문제가되어서는 안됩니다.


BTW, 당신이 사용하고 동기화 기술은 Seqlock이지만, 단일 - 작가와. 별도의 작성자를 동기화하는 잠금 부분이 아니라 시퀀스 부분 만 있습니다. 멀티 라이터 버전에서는 일련 번호와 데이터를 읽고 쓰기 전에 작성자가 잠금을 해제합니다. (그리고 seq no를 함수 인자로 사용하는 대신 자물쇠에서 읽는다.)

C++ 표준 토론 자료 N4455 (예 : 원자로의 컴파일러 최적화에 대해서는 Can num++ be atomic for 'int num'?에서 내 대답의 후반부 참조)는이를 예로 사용합니다.

StoreStore 펜스 대신 작성자의 데이터 항목에 릴리스 저장을 사용합니다. (원자 데이터 항목으로, 내가 말했듯이 이것이 실제로 맞아야한다.)

void writer(T d1, T d2) { 
    unsigned seq0 = seq.load(std::memory_order_relaxed); // note that they read the current value because it's presumably a multiple-writers implementation. 
    seq.store(seq0 + 1, std::memory_order_relaxed); 
    data1.store(d1, std::memory_order_release); 
    data2.store(d2, std::memory_order_release); 
    seq.store(seq0 + 2, std::memory_order_release); 
} 

그들은 잠재적으로 컴파일러가 그렇게하고, 잠재적 인 방법으로 독자에 t2 = seq.fetch_add(0, std::memory_order_release)를 사용하여과 부하를 얻을하는 것이 수익성이 있는지, 나중에 작업과 순서 시퀀스 번호의 독자의 두 번째 부하를시키는 이야기 릴리스 의미. 현재 컴파일러를 사용하면 이 아닌을 권장합니다. 당신은 lock ed 작업을 x86에서 얻을 수 있습니다. 위에서 제안한 방식에는 전체 장벽 seq_cst 울타리가 x86에 대한 지침이 필요하기 때문에 실제 장벽 명령이 없습니다.

+0

@ cmaster : 릴리스 저장소는 다른 모든 저장소를 주문하는 양방향 StoreStore 장벽이라고 생각할 것입니다. Jeff Preshing이이 기사에서 설명하는 것처럼 (http://preshing.com/20131125/acquire-and-release-fences-dont-work-theway-youd-expect/), 그는 설명하지 않습니다. 릴리스 저장소와 독립 실행 형'atomic_thread_fence (mo_release) '사이의 구별은 장벽입니다. 릴리즈 - 스토어는 일방적 인 장벽이며 획득로드의 경우에도 마찬가지이며, Jeff의 기사는 이것을 언급하는 몇 가지 다른 기사/회담으로 연결됩니다. –

+0

아참. 네, 물론, 당신 말이 맞아요. 죄송합니다, 내 코멘트를 삭제했습니다. 불행히도 더 이상 내 downvote를 제거 할 수 없습니다 :-( – cmaster

+0

@ cmaster : 수정 실수를 발견하여 첫 번째 단락에서 확장하여 향후 독자가 "이 소리가 잘못 들었습니다"라는 느낌으로 시작하지 못하게 할 수 있습니다. 당신은 당신의 표를 바꿀 수 있습니다 :) –