수색…


비고

적어도 하나의 연산이 수정 ( 스토어 연산 이라고도 함)되는 경우 동일한 메모리 위치에 액세스하려는 다른 스레드가 데이터 경쟁에 참여합니다. 이러한 데이터 경쟁으로 인해 정의되지 않은 동작이 발생 합니다. 이를 피하기 위해 이러한 스레드가 충돌하는 작업을 동시에 실행하지 못하도록해야합니다.

동기화 프리미티브 (뮤텍스, 크리티컬 섹션 등)는 이러한 액세스를 보호 할 수 있습니다. C ++ 11에 소개 된 메모리 모델은 다중 스레드 환경에서 메모리에 대한 액세스를 원자 적 연산과 펜스와 동기화하는 두 가지 새로운 방법을 정의합니다.

원자력 운영

원자 적로드원자 저장소 작업을 사용하여 지정된 메모리 위치를 읽고 쓸 수 있습니다. 편의상 이것들은 std::atomic<t> 템플릿 클래스에 싸여 있습니다. 이 클래스는 t 유형의 값을 래핑하지만 이번에는 객체에 대한 로드저장 은 원 자성입니다.

템플릿은 모든 유형에 사용할 수있는 것은 아닙니다. 사용할 수있는 유형은 구현에 따라 다르지만 일반적으로 대부분의 (또는 전체) 사용 가능한 정수 유형과 포인터 유형이 포함됩니다. 따라서 std::atomic<std::pair<bool,char>> 아마도 사용되지 않을 것이지만 std::atomic<unsigned>std::atomic<std::vector<foo> *> 를 사용할 수 있어야합니다.

원자 연산의 속성은 다음과 같습니다.

  • 정의되지 않은 동작을 발생시키지 않으면 서 모든 원자 연산을 여러 스레드에서 동시에 수행 할 수 있습니다.
  • 원자 적재 는 원자 적 객체가 생성 된 초기 값 또는 원자 적 저장 연산을 통해 그 값에 기록 된 값을 보게 될 것이다.
  • 동일한 원자 객체에 대한 원자 저장소 는 모든 스레드에서 동일하게 정렬됩니다. 쓰레드가 어떤 원자 저장소 연산의 값을 이미 본다면, 이후의 원자 적재 연산은 동일한 값 또는 후속 원자 저장소 연산에 의해 저장된 값을 보게됩니다.
  • 원자 읽기 - 수정 - 쓰기 작업은 원자로드원자 저장소 사이에 다른 원자를 저장하지 않고 일어날 수 있습니다. 예를 들어, 하나는 여러 스레드에서 카운터를 원자 적으로 증가시킬 수 있으며 스레드 간의 충돌에 관계없이 증가분은 손실되지 않습니다.
  • 원자 연산은 다른 메모리 위치와 관련하여 연산이 갖는 추가 속성을 정의하는 선택적 std::memory_order 매개 변수를받습니다.
std :: memory_order 의미
std::memory_order_relaxed 추가 제한 없음
std::memory_order_releasestd::memory_order_acquire 경우 load-acquire 저장 값을보고 store-release전에 순서가 저장 store-release 부하 획득 한 후 염기 서열을로드되기 전에 발생
std::memory_order_consume memory_order_acquire 와 유사하지만 종속로드에 대해서만
std::memory_order_acq_rel load-acquirestore-release
std::memory_order_seq_cst 순차적 일관성

이러한 메모리 순서 태그는 세 가지 다른 메모리 순서 지정 분야를 허용합니다. 즉, 순차적 일관성 , 완화 및 형제와 함께 릴리스 획득릴리스를 사용 합니다.

순차 일관성

원자 조작에 대해 메모리 순서가 지정되지 않은 경우, 순서는 순차 일관성을 기본값 으로 합니다. 이 모드는 std::memory_order_seq_cst 작업 태그를 지정하여 명시 적으로 선택할 수도 있습니다.

이 순서로 메모리 조작은 원자 조작을 넘을 수 없습니다. 모든 메모리 연산은 원자 연산과 원자 연산이 발생하기 전에 원자 연산이 발생하기 전에 일어난다. 이 모드는 아마도 가장 쉬운 방법 일 수 있지만 성능에 가장 큰 불이익을 초래합니다. 또한 원자 연산을 지나서 연산을 재정렬하려고 시도하는 모든 컴파일러 최적화를 방지합니다.

편안한 주문

순차적 일관성 의 반대는 편안한 메모리 정렬입니다. std::memory_order_relaxed 태그로 선택됩니다. Relaxed atomic 연산은 다른 메모리 연산에 대한 제한을 부과하지 않는다. 여전히 남아있는 유일한 효과는 조작 자체가 여전히 원자 적이라는 것입니다.

주문 취소 주문

원자 저장소 연산에는 std::memory_order_release 태그를 지정할 수 있으며 원자 std::memory_order_release 작업에는 std::memory_order_acquire 태그를 지정할 수 있습니다. 첫 x 째 조작은 (원자) 저장 해제 라고하며 두 x 째 조작은 (원자) load-acquire라고 합니다.

load-acquire상점 릴리스에 의해 쓰여진 값을 보게되면 다음과 같은 결과가 발생합니다. 상점 릴리스로드 획득 전에 순서화 된로드 조작에 대해 가시적으로되기 전에 모든 저장 조작이 순서화됩니다.

원자 읽기 - 수정 - 쓰기 연산은 또한 누적 태그 std::memory_order_acq_rel 받을 수있다. 이렇게하면 작업의 원자 적로드 부분이 원자 적재량을 획득하게 되고 원자 저장량 부분은 원자력 저장 방출이 됩니다.

컴파일러는 원자 상점 릴리스 조작 후에 상점 조작을 이동할 수 없습니다. 또한 원자 적로드 획득 (또는 로드 소비 ) 전에로드 조작을 이동하는 것이 허용되지 않습니다.

또한 원자 적재 - 방출 또는 원자 축적 - 획득 이 없음에 유의하십시오. 이러한 작업을 만들려고하면 작업이 느슨해 집니다.

릴리즈 - 소비 주문

이러한 결합 해제-획득을 유사하지만,이 시간은 원자 부하가 태그되는 std::memory_order_consume(원자) 부하 소비 동작된다. 이 모드는 서열화 후에로드 동작 중 이들에 의해로드 된 값에 따라 부하만을 소비하는 것이 유일한 차이점 - 취득 떼면 동일한 부하 소비하는 정렬된다.

울타리

펜스는 또한 스레드간에 메모리 연산을 정렬 할 수있게합니다. 울타리는 릴리스 울타리 또는 취득 울타리 중 하나입니다.

획득 펜스가 획득 펜스보다 먼저 발생하면 펜스가 획득 펜스 이후에 시퀀싱 된로드에 표시되기 전에 시퀀스가 ​​저장됩니다. 획득 울타리 이전에 릴리스 펜스가 발생하도록 보장하기 위해 완화 된 원자 연산을 비롯한 다른 동기화 프리미티브를 사용할 수 있습니다.

메모리 모델 필요

int x, y;
bool ready = false;

void init()
{
  x = 2;
  y = 3;
  ready = true;
}
void use()
{
  if (ready)
    std::cout << x + y;
}

한 스레드는 init() 함수를 호출하고 다른 스레드 (또는 신호 처리기)는 use() 함수를 호출합니다. use() 함수는 5 를 인쇄하거나 아무것도하지 않을 것이라고 예상 할 수 있습니다. 다음과 같은 여러 가지 이유로 항상 그런 것은 아닙니다.

  • CPU는 실제로 실행되는 코드가 다음과 같이 보이도록 init() 에서 발생하는 쓰기 순서를 변경할 수 있습니다.

    void init()
    {
      ready = true;
      x = 2;
      y = 3;
    }
    
  • CPU는 실제로 실행 된 코드가 다음과 같이 될 수 있도록 use() 에서 발생하는 읽기 순서를 변경할 수 있습니다.

    void use()
    {
      int local_x = x;
      int local_y = y;
      if (ready)
        std::cout << local_x + local_y;
    }
    
  • 최적화 된 C ++ 컴파일러는 비슷한 방식으로 프로그램을 재정렬하기로 결정할 수 있습니다.

스레드가 init()use() 대한 호출을 인터리브 할 수 없기 때문에 그러한 순서 재 지정은 단일 스레드에서 실행중인 프로그램의 동작을 변경할 수 없습니다. 반면에 다중 스레드 설정에서는 한 스레드가 다른 스레드가 수행하는 쓰기 작업의 일부를 볼 수 있습니다. use()ready==true , x 또는 y 또는 둘 다에서 쓰레기를 볼 수 있습니다.

C ++ 메모리 모델은 프로그래머가 어떤 순서 변경 작업이 허용되는지 아닌지를 지정할 수있게하여 다중 스레드 프로그램이 예상대로 작동 할 수 있도록합니다. 위의 예제는 다음과 같이 스레드로부터 안전한 방식으로 다시 작성할 수 있습니다.

int x, y;
std::atomic<bool> ready{false};

void init()
{
  x = 2;
  y = 3;
  ready.store(true, std::memory_order_release);
}
void use()
{
  if (ready.load(std::memory_order_acquire))
    std::cout << x + y;
}

init()원자 적 저장 - 해제 연산을 수행한다. 이것은 ready 값에 true 값을 저장하는 것뿐만 아니라 컴파일러에게 이전에 순서화 된 쓰기 조작 전에이 조작을 이동할 수 없음을 알려줍니다.

use() 함수는 원자 적중량 취득 작업을 수행합니다. ready 의 현재 값을 읽고 컴파일러가 원자 적재를 획득 하기 전에 순서대로 읽는 연산을 배치하는 것을 금지합니다.

이러한 원자 연산은 컴파일러로 하여금 CPU에 알리기 위해 필요한 하드웨어 명령어를 넣고 불필요한 재 배열을 삼 간다.

atomic store-releaseatomic load-acquire 와 동일한 메모리 위치이기 때문에, 메모리 모델은 load-acquire 연산이 store-release 연산에 의해 쓰여진 값을 보게되면 init() 의해 수행 된 모든 쓰기가 해당 store-release 이전의 쓰레드는 use() 의 쓰레드가 로드를 획득 한 후에 실행하는 로드에서 볼 수있다 . 즉, use()ready==true 를 보면 x==2y==3 있습니다.

컴파일러와 CPU는 여전히 x 쓰기 전에 y 에 쓸 수 있으며, 마찬가지로 use() 에서 이러한 변수의 읽기는 어떤 순서로도 발생할 수 있습니다.

울타리 예제

위의 예제는 펜스 (fence) 및 편안한 원자 연산으로 구현 될 수도 있습니다.

int x, y;
std::atomic<bool> ready{false};

void init()
{
  x = 2;
  y = 3;
  atomic_thread_fence(std::memory_order_release);
  ready.store(true, std::memory_order_relaxed);
}
void use()
{
  if (ready.load(std::memory_order_relaxed))
  {
    atomic_thread_fence(std::memory_order_acquire);
    std::cout << x + y;
  }
}

원자 적재 연산이 원자 저장소에 의해 쓰여진 값을보고 부하가 발생하기 전에 저장소가 발생하면 펜스도 그렇게합니다 : 획득 펜스가 획득 펜스보다 먼저 발생하여 펜스가 표시되기 전에 펜스가 xy 기록됩니다 획득 울타리 다음에 나오는 std::cout 문으로 이동합니다.

울타리는 획득, 릴리스 또는 기타 동기화 작업의 전체 수를 줄일 수 있다면 유용 할 수 있습니다. 예 :

void block_and_use()
{
  while (!ready.load(std::memory_order_relaxed))
    ;
  atomic_thread_fence(std::memory_order_acquire);
  std::cout << x + y;
}

block_and_use() 함수는 ready 플래그가 편안한 원자 적로드의 도움으로 설정 될 때까지 회전합니다. 그런 다음 하나의 획득 펜스가 필요한 메모리 순서를 제공하는 데 사용됩니다.



Modified text is an extract of the original Stack Overflow Documentation
아래 라이선스 CC BY-SA 3.0
와 제휴하지 않음 Stack Overflow