다음과 같이 간단한 멀티 스레딩 프로그램을 작성했습니다.
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;
}
완드 박스에 라이브