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

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

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.



답변

비 원자, 비 보호 변수를 액세스하는 두 개의 스레드가 있습니다 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 센트를 소비하고 싶다.


답변

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가 하나가되어 있어야하는 것입니다.

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


답변

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

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

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;
}

완드 박스에 라이브