멀티 스레딩 프로그램이 최적화 모드에서 멈췄지만 -O0에서 정상적으로 실행 됨


68

다음과 같이 간단한 멀티 스레딩 프로그램을 작성했습니다.

static bool finished = false;

int func()
{
    size_t i = 0;
    while (!finished)
        ++i;
    return i;
}

int main()
{
    auto result=std::async(std::launch::async, func);
    std::this_thread::sleep_for(std::chrono::seconds(1));
    finished=true;
    std::cout<<"result ="<<result.get();
    std::cout<<"\nmain thread id="<<std::this_thread::get_id()<<std::endl;
}

그것은 디버그 모드로 정상적으로 동작 비주얼 스튜디오-O0에서 GC C와 후 결과를 인쇄 1초. 그러나 릴리즈 모드에서 또는 -O1 -O2 -O3.


의견은 긴 토론을위한 것이 아닙니다. 이 대화는 채팅 으로 이동 되었습니다 .
Samuel Liew

답변:


100

비 원자, 비 보호 변수를 액세스하는 두 개의 스레드가 있습니다 UB에게 이 문제 finished. 이 문제를 해결하기 위해 finished유형 std::atomic<bool>을 만들 수 있습니다.

내 수정 :

#include <iostream>
#include <future>
#include <atomic>

static std::atomic<bool> finished = false;

int func()
{
    size_t i = 0;
    while (!finished)
        ++i;
    return i;
}

int main()
{
    auto result=std::async(std::launch::async, func);
    std::this_thread::sleep_for(std::chrono::seconds(1));
    finished=true;
    std::cout<<"result ="<<result.get();
    std::cout<<"\nmain thread id="<<std::this_thread::get_id()<<std::endl;
}

산출:

result =1023045342
main thread id=140147660588864

콜리 루 라이브 데모


누군가 ' bool아마도 1 비트 일 것입니다. 이것이 어떻게 원자가 아닌가? ' (멀티 스레딩을 시작했을 때했습니다.)

그러나 찢어지지 않는 것이 유일한 것은 아닙니다. std::atomic 이 당신에게주는 . 또한 여러 스레드에서 동시에 읽기 + 쓰기 액세스를 명확하게 정의하여 변수를 다시 읽을 때 항상 동일한 값을 볼 것이라고 가정하지 않습니다.

bool보호되지 않은 비원자를 만들면 추가 문제가 발생할 수 있습니다.

  • 컴파일러는 변수를 레지스터로 최적화하거나 하나에 대한 CSE 다중 액세스를 최적화하고 루프에서로드를 끌어 올릴 수 있습니다.
  • 변수는 CPU 코어에 대해 캐시 될 수 있습니다. (실제 생활에서, CPU는 일관된 캐시를 가지고 . 이것은 진짜 문제가 아니라 C ++ 표준 ++ 비 간섭 공유 메모리에 구현 가상 C를 커버하는 느슨한 충분 atomic<bool>memory_order_relaxed저장 /로드가 작동합니다,하지만 어디는 volatile하지 않을 것입니다. 사용 실제 C ++ 구현에서 실제로 작동하더라도 UB는 일시적입니다.)

이를 방지하려면 컴파일러에게 명시 적으로 수행하지 말 것을 지시해야합니다.


volatile이 문제 와의 잠재적 관계에 관한 진화론에 대해 약간 놀랐습니다 . 따라서 나는 2 센트를 소비하고 싶다.


4
나는 한 번 살펴보고 func()"나는 그것을 최적화 할 수있다"고 생각했습니다. 옵티마이 저는 스레드를 전혀 신경 쓰지 않고 무한 루프를 감지하고 행복하게 "while (True)"로 바꿉니다. .org / z / Tl44iN 우리는 이것을 볼 수 있습니다. 완료되면 True반환됩니다. 그렇지 않은 경우 레이블에서 무조건 점프합니다 (무한 루프).L5
Baldrickk


2
@val : and와 volatile동일한 asm을 얻을 수 있기 때문에 기본적으로 C ++ 11에서 남용 할 이유가 없습니다 . 실제 하드웨어에서는 작동하지만 캐시는 일관성이 있으므로 다른 코어의 저장소가 캐시를 커밋하면로드 명령이 오래된 값을 계속 읽을 수 없습니다. (MESI)atomic<T>std::memory_order_relaxed
Peter Cordes

5
@PeterCordes Using volatile는 여전히 UB입니다. UB는 그것이 잘못 될 수있는 방법을 생각할 수없고 시도했을 때 효과가 있었기 때문에 UB가 안전하고 확실하다고 생각해서는 안됩니다. 사람들이 계속 불타고 있습니다.
David Schwartz

2
@Damon Mutexes는 시맨틱을 출시 / 취득했습니다. 뮤텍스 이전에 잠긴 경우 컴파일러는 그렇게 보호, 멀리 읽기를 최적화 할 수 없습니다 finished로모그래퍼 std::mutex작업 (없이 volatile또는 atomic). 실제로 모든 원자를 "단순"값 + 뮤텍스 방식으로 바꿀 수 있습니다. 그것은 여전히 ​​작동하고 느릴 것입니다. atomic<T>내부 뮤텍스를 사용할 수 있습니다. atomic_flag잠금이없는 것만 보장 됩니다 .
Erlkoenig

42

Scheff의 답변은 코드를 수정하는 방법을 설명합니다. 나는이 경우 실제로 일어나는 일에 대해 약간의 정보를 추가 할 것이라고 생각했습니다.

최적화 수준 1 ( )을 사용하여 godbolt 에서 코드를 컴파일했습니다 -O1. 함수는 다음과 같이 컴파일됩니다.

func():
  cmp BYTE PTR finished[rip], 0
  jne .L4
.L5:
  jmp .L5
.L4:
  mov eax, 0
  ret

그래서 여기서 무슨 일이 일어나고 있습니까? 먼저, 우리는 비교를합니다 : cmp BYTE PTR finished[rip], 0-이것은 finished거짓인지 아닌지를 확인합니다.

거짓 이 아닌 경우 (일명 true) 첫 번째 실행에서 루프를 종료해야합니다. 이렇게함으로써 달성 jne .L4되는 j 개의 umps N OT 전자 라벨 QUAL .L4의 값이 여기서 i( 0) 이상 사용 함수가 리턴하는 레지스터에 저장된다.

이 경우 이다 그러나 거짓, 우리는로 이동

.L5:
  jmp .L5

이것은 .L5점프 명령 자체 가되는 무조건 점프 입니다.

즉, 스레드는 무한 사용중 루프에 놓입니다.

왜 이런 일이 일어 났습니까?

옵티 마이저와 관련하여 스레드는 그 범위를 벗어납니다. 다른 스레드가 변수를 동시에 읽거나 쓰지 않는다고 가정합니다 (데이터 레이스 UB이기 때문에). 액세스를 최적화 할 수 없다는 것을 알려야합니다. 이것은 Scheff의 대답이 나오는 곳입니다. 나는 그를 반복하지 않을 것입니다.

옵티마이 저는 finished함수를 실행하는 동안 변수가 잠재적으로 변경 될 수 있다고 알려주지 않기 때문에 finished함수 자체에 의해 수정되지 않고 일정하다고 가정합니다.

최적화 된 코드는 일정한 bool 값으로 함수를 입력하여 발생하는 두 가지 코드 경로를 제공합니다. 루프를 무한대로 실행하거나 루프가 실행되지 않습니다.

-O0(예상) 컴파일러 떨어져 루프 본체와 비교하여 최적화되지 않는다 :

func():
  push rbp
  mov rbp, rsp
  mov QWORD PTR [rbp-8], 0
.L148:
  movzx eax, BYTE PTR finished[rip]
  test al, al
  jne .L147
  add QWORD PTR [rbp-8], 1
  jmp .L148
.L147:
  mov rax, QWORD PTR [rbp-8]
  pop rbp
  ret

따라서 최적화되지 않은 함수가 작동 할 때 코드와 데이터 유형이 단순하기 때문에 원자 성의 부족은 일반적으로 문제가되지 않습니다. 아마도 우리가 여기서 겪을 수있는 최악의 상황은 그 가치 i가 하나가되어 있어야하는 것입니다.

데이터 구조가있는보다 복잡한 시스템은 데이터가 손상되거나 실행이 잘못 될 가능성이 훨씬 높습니다.


3
C ++ 11은 스레드와 스레드 인식 메모리 모델을 언어 자체의 일부로 만듭니다. 이것은 컴파일러가 atomic해당 변수를 작성하지 않는 코드의 비 변수에 대한 쓰기도 발명 할 수 없음을 의미 합니다. 예를 들어 ,로드 + 스토어 (원자 RMW 아님)가 다른 스레드에서 쓰기를 수행 할 수 있기 때문에 if (cond) foo=1;asm으로 변환 할 수 없습니다 foo = cond ? 1 : foo;. 그들은 멀티 스레드 프로그램을 작성하는 데 유용 할 싶었 기 때문에 컴파일러는 이미 같은 물건을 피했지만, C ++ (11)는 컴파일러가 2 개 스레드가 쓰기 곳에 코드를 아프게하지했다가 공식 것을 만들어 a[1]a[2]
피터 코르에게

2
그러나 그렇습니다. 컴파일러가 스레드 를 전혀 인식하지 않는 방법에 대한 과장된 표현 외에는 정답입니다. Data-race UB는 전역 변수 및 단일 스레드 코드에 대해 원하는 다른 공격적인 최적화를 포함하여 많은 비 원자 변수를 게양 할 수있게 해줍니다. MCU 프로그래밍-루프 온 전자 장치에서 C ++ O2 최적화가 중단 됩니다 .SE는이 설명의 제 버전입니다.
Peter Cordes

1
@PeterCordes하십시오 GC를 사용하여 자바의 장점 중 하나는 개체에 대한 해당 메모리가 개입하지 않고 재활용 할 수 없습니다입니다 세계 어떤 핵심 시험 일정 객체가 항상 가지고 어떤 가치를 볼 수 있다는 것을 의미 신구 사용 사이의 메모리 장벽, 참조가 처음 게시 된 후 언젠가 개최되었습니다. 전역 메모리 장벽은 자주 사용하는 경우 매우 비쌀 수 있지만, 조금만 사용하더라도 다른 곳에서 메모리 장벽의 필요성을 크게 줄일 수 있습니다.
supercat

1
그렇습니다, 당신이 말하려는 것을 알고 있었지만, 당신의 말이 100 %라는 의미는 아닙니다. 옵티 마이저를 말하면 "완전히 무시합니다." 잘 맞지 않습니다 : 최적화 할 때 실제로 스레딩을 무시하면 단어로드 / 단어 저장소의 바이트 수정과 같은 것들이 포함될 수 있습니다. 실제로 하나의 스레드가 char 또는 bitfield 단계에 액세스 할 때 버그가 발생했습니다. 인접한 구조체 멤버에 씁니다. 전체 내용은 lwn.net/Articles/478657 을 참조하십시오 . C11 / C ++ 11 메모리 모델 만이 그러한 최적화를 실제로 원하지 않는 것이 아니라 불법으로 만드는 방법을 참조하십시오.
Peter Cordes

1
아뇨, 좋습니다. 감사합니다 @PeterCordes. 개선에 감사드립니다.
Baldrickk

5

학습 곡선의 완전성을 위해; 전역 변수를 사용하지 않아야합니다. 정적으로 만들어서 잘 했으므로 번역 단위에 로컬이됩니다.

예를 들면 다음과 같습니다.

class ST {
public:
    int func()
    {
        size_t i = 0;
        while (!finished)
            ++i;
        return i;
    }
    void setFinished(bool val)
    {
        finished = val;
    }
private:
    std::atomic<bool> finished = false;
};

int main()
{
    ST st;
    auto result=std::async(std::launch::async, &ST::func, std::ref(st));
    std::this_thread::sleep_for(std::chrono::seconds(1));
    st.setFinished(true);
    std::cout<<"result ="<<result.get();
    std::cout<<"\nmain thread id="<<std::this_thread::get_id()<<std::endl;
}

완드 박스에 라이브


1
함수 블록 내 finished에서처럼 선언 할 수도 있습니다 static. 여전히 한 번만 초기화되며 상수로 초기화되면 잠금이 필요하지 않습니다.
Davislor

액세스는 finished더 저렴한 std::memory_order_relaxed로드 및 저장을 사용할 수도 있습니다 . WRT 주문이 필요하지 않습니다. 스레드에서 다른 변수. static그러나 @Davislor의 제안 이 의미가 있는지 확실하지 않습니다 . 스핀 카운트 스레드가 여러 개인 경우 동일한 플래그로 스레드를 모두 중지하지 않아도됩니다. finished그래도 원자 저장소가 아닌 초기화로 컴파일되는 방식으로 초기화를 작성하려고합니다 . ( finished = false;기본 이니셜 라이저 C ++ 17 구문을 사용하는 것처럼 godbolt.org/z/EjoKgq ).
피터 코데

@PeterCordes 객체에 플래그를 넣으면 다른 스레드 풀에 대해 둘 이상이있을 수 있습니다. 그러나 원래 디자인에는 모든 스레드에 대해 단일 플래그가있었습니다.
Davislor
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.