고전적인 문제의 변형을 해결하는 휴대용 코드 (Intel, ARM, PowerPC ...)를 작성하고 싶습니다.
Initially: X=Y=0
Thread A:
X=1
if(!Y){ do something }
Thread B:
Y=1
if(!X){ do something }
하는 목표는 모두 스레드가 수행되는 상황을 방지하는 것입니다something
. (아무것도 실행되지 않으면 괜찮습니다. 이것은 한 번만 실행되는 메커니즘이 아닙니다.) 아래의 내 추론에 약간의 결함이 있으면 나를 수정하십시오.
나는 다음과 같이 memory_order_seq_cst
atomic store
과 load
s로 목표를 달성 할 수 있음을 알고 있습니다.
std::atomic<int> x{0},y{0};
void thread_a(){
x.store(1);
if(!y.load()) foo();
}
void thread_b(){
y.store(1);
if(!x.load()) bar();
}
{x.store(1), y.store(1), y.load(), x.load()}
프로그램 순서 "에지 (edge)"와 일치해야하는 이벤트 에 총 단일 순서가 있어야하기 때문에 목표를 달성합니다 .
x.store(1)
"TO TO 이전"y.load()
y.store(1)
"TO TO 이전"x.load()
foo()
부름을 받았다 면 추가 우위가 있습니다.
y.load()
"전에 가치를 읽는다"y.store(1)
bar()
부름을 받았다 면 추가 우위가 있습니다.
x.load()
"전에 가치를 읽는다"x.store(1)
이 모든 모서리가 함께 결합되어 사이클을 형성합니다.
x.store(1)
"TO TO before before" y.load()
"전에 값을 읽습니다 y.store(1)
."TO TO before before " x.load()
"전에 값을 읽습니다 "x.store(true)
주문에주기가 없다는 사실을 위반합니다.
나는 의도적으로 비표준 용어를 "TO TO before before"와 같은 표준 용어와 달리 "Before value before"를 사용합니다 happens-before
. 왜냐하면 이러한 에지가 실제로 happens-before
관계를 암시한다는 가정의 정확성에 대한 피드백을 요청하고 싶기 때문입니다. 이러한 결합 그래프의주기는 금지되어 있습니다. 확실하지 않습니다. 내가 아는 것은이 코드가 Intel gcc & clang 및 ARM gcc에서 올바른 장벽을 생성한다는 것입니다
이제 실제 문제는 "X"를 제어 할 수 없기 때문에 조금 더 복잡합니다. 일부 매크로, 템플릿 등에 숨겨져 있고보다 약할 수 있습니다. seq_cst
"X"가 단일 변수인지 아니면 다른 개념 (예 : 가벼운 세마포어 또는 뮤텍스)인지조차 모르겠습니다. 내가 아는 것은 두 개의 매크로가 set()
있고 다른 스레드가 호출 한 후 "후" check()
를 check()
반환 한다는 것 입니다. (그것은 되어 도하는 것으로 알려져 및 스레드 안전 및 데이터 레이스 UB를 만들 수 없습니다.)true
set()
set
check
개념적으로 set()
는 "X = 1"과 check()
비슷하고 "X"와 비슷하지만 관련된 원자에 직접 접근 할 수는 없습니다.
void thread_a(){
set();
if(!y.load()) foo();
}
void thread_b(){
y.store(1);
if(!check()) bar();
}
나는 걱정하고있어 set()
내부적으로 구현 될 수 x.store(1,std::memory_order_release)
및 / 또는 check()
수 있습니다 x.load(std::memory_order_acquire)
. 또는 std::mutex
한 스레드가 잠금 해제되고 다른 스레드가 잠금 해제된다는 가정 try_lock
이 있습니다. ISO 표준 std::mutex
에서는 seq_cst가 아닌 획득 및 릴리스 순서 만 보장합니다.
이 경우 check()
이전에 "본문을 다시 정렬"할 수있는 경우입니다 y.store(true)
( Alex의 답변을 참조하십시오 ( PowerPC에서 발생 함 )).
이 이벤트 시퀀스가 가능하므로 이것은 실제로 나쁠 것입니다.
thread_b()
먼저x
(0
) 의 이전 값을로드합니다.thread_a()
포함하여 모든 것을 실행foo()
thread_b()
포함하여 모든 것을 실행bar()
그래서, 모두 foo()
와는 bar()
내가 피할 수 있던라고있어. 이를 방지 할 수있는 옵션은 무엇입니까?
옵션 A
Store-Load 장벽을 강제로 시도하십시오. 실제로 이것은 Alex가 다른 답변에서std::atomic_thread_fence(std::memory_order_seq_cst);
설명한 것처럼 모든 테스트 된 컴파일러가 완전한 울타리를 방출 했다고 설명합니다 .
- x86_64 : MFENCE
- PowerPC : hwsync
- 이타 누임 : MF
- ARMv7 / ARMv8 : dmb ish
- MIPS64 : 동기화
이 접근법의 문제점은 C ++ 규칙에서 std::atomic_thread_fence(std::memory_order_seq_cst)
완전한 메모리 장벽으로 변환해야 한다는 보장을 찾을 수 없다는 것 입니다. 실제로, atomic_thread_fence
C ++에서 s의 개념은 메모리 장벽의 조립 개념과는 다른 추상화 수준 인 것으로 보이며 "어떤 원자 연산이 무엇과 동기화되는지"와 같은 것들을 더 다루고 있습니다. 아래 구현이 목표를 달성했다는 이론적 증거가 있습니까?
void thread_a(){
set();
std::atomic_thread_fence(std::memory_order_seq_cst)
if(!y.load()) foo();
}
void thread_b(){
y.store(true);
std::atomic_thread_fence(std::memory_order_seq_cst)
if(!check()) bar();
}
옵션 B
Y에 대한 read-modify-write memory_order_acq_rel 작업을 사용하여 동기화를 달성하기 위해 Y에 대한 제어를 사용하십시오.
void thread_a(){
set();
if(!y.fetch_add(0,std::memory_order_acq_rel)) foo();
}
void thread_b(){
y.exchange(1,std::memory_order_acq_rel);
if(!check()) bar();
}
여기서 아이디어는 단일 원자 ( y
) 에 대한 액세스는 모든 관찰자가 동의하는 단일 순서를 형성해야하므로 fetch_add
이전 exchange
또는 그 반대입니다.
fetch_add
이전 인 경우 exchange
"release"부분은 fetch_add
"acquire"부분과 동기화 exchange
되므로 set()
코드 실행시 모든 부작용을 볼 수 있어야 check()
하므로 bar()
호출되지 않습니다.
그렇지 않으면, exchange
이전이다 fetch_add
다음은 fetch_add
볼 1
과 전화하지 foo()
. 따라서 foo()
와를 모두 호출하는 것은 불가능합니다 bar()
. 이 추론이 맞습니까?
옵션 C
더미 원자를 사용하여 재난을 방지하는 "가장자리"를 도입하십시오. 다음 접근법을 고려하십시오.
void thread_a(){
std::atomic<int> dummy1{};
set();
dummy1.store(13);
if(!y.load()) foo();
}
void thread_b(){
std::atomic<int> dummy2{};
y.store(1);
dummy2.load();
if(!check()) bar();
}
여기에 문제 atomic
가 국지적 이라고 생각하면 다음과 같은 추론에서 문제를 전역 범위로 옮기는 것을 상상해보십시오. 나는 의도적으로 더미가 얼마나 재미 있는지 노출시키는 방식으로 코드를 썼습니다. dummy2는 완전히 분리되어 있습니다.
왜 지구상에서 이것이 효과가 있을까요? 글쎄, {dummy1.store(13), y.load(), y.store(1), dummy2.load()}
프로그램 순서 "에지"와 일치 해야하는 단일 전체 순서가 있어야합니다 .
dummy1.store(13)
"TO TO 이전"y.load()
y.store(1)
"TO TO 이전"dummy2.load()
(seq_cst store + load는 StoreLoad를 포함한 전체 메모리 장벽과 동등한 C ++을 형성하기를 희망합니다. 별도의 장벽 명령이 필요하지 않은 AArch64를 포함한 실제 ISA에서 asm에서 asm에서와 마찬가지로)
이제 우리는 두 가지 경우를 고려해야합니다. 총 순서 y.store(1)
이전 y.load()
또는 이후입니다.
경우 y.store(1)
이전 인 y.load()
다음 foo()
이라고 우리는 안전하지 않습니다.
경우 y.load()
이전이다 y.store(1)
, 우리는 이미 프로그램 순서에서이 두 가장자리과 결합, 우리는 것을 추론 :
dummy1.store(13)
"TO TO 이전"dummy2.load()
이제,이 dummy1.store(13)
효과를 해제 릴리스 동작입니다 set()
, 그리고 dummy2.load()
그래서, 획득 작업입니다 check()
의 효과를 볼 수 set()
있어 bar()
호출되지 않습니다 우리는 안전합니다.
check()
그 결과를 볼 것이라고 생각하는 것이 맞 set()
습니까? 이렇게 다양한 종류의 "가장자리"( "순서 순서", "전체 순서", "해제 전", "취득 후")를 결합 할 수 있습니까? 나는 이것에 대해 심각한 의문을 가지고 있습니다 : C ++ 규칙은 같은 위치에서 상점과로드 사이의 "동기화"관계에 대해 이야기하는 것 같습니다-여기에는 그러한 상황이 없습니다.
우리는 seq_cst 총 순서에서 (다른 추론을 통해 dumm1.store
) 알려진 것으로 알려진 경우에만 걱정합니다 dummy2.load
. 따라서 동일한 변수에 액세스 한 경우로드에 저장된 값이 표시되고 동기화됩니다.
(atomic loads 및 store가 적어도 일방 메모리 장벽으로 컴파일되는 구현에 대한 메모리 배리어 / 리오 더링 추론 (및 seq_cst 작업은 순서를 바꿀 수 없습니다 : 예를 들어 seq_cst store는 seq_cst로드를 통과 할 수 없음)은 모든로드 / 이후 dummy2.load
에 다른 스레드에 확실히 표시되고 나중에 다른 스레드에 y.store
대해서도 유사하게 저장됩니다 y.load
.
https://godbolt.org/z/u3dTa8 에서 옵션 A, B, C를 구현할 수 있습니다.
foo()
및 bar()
호출 하지 마십시오 .
compare_exchange_*
원자 부울에서 값을 변경하지 않고 RMW 작업을 수행하는 데 사용할 수 있습니다 (예상 값과 새 값을 같은 값으로 간단히 설정).
atomic<bool>
has exchange
및 compare_exchange_weak
. 후자는 CAS (true, true) 또는 false, false를 시도하여 더미 RMW를 수행하는 데 사용될 수 있습니다. 실패하거나 원자 적으로 값을 자체로 바꿉니다. (x86-64 asm에서, 그 트릭 lock cmpxchg16b
은 16 바이트로드를 보장하는 방법입니다. 비효율적이지만 별도의 잠금을 취하는 것보다 덜 나쁩니다.)
foo()
받지 bar()
않을 수도 있습니다. 나는 코드의 많은 "실제 세계"요소들을 가져오고 싶지 않았다. 그러나 하나가 정말 배경 층이 무엇인지 알 필요가있는 경우 : set()
정말 some_mutex_exit()
, check()
있다 try_enter_some_mutex()
, y
"일부 웨이터가"되어 foo()
"사람을 깨어하지 않고 종료"입니다, bar()
나는 거부 "wakup에 대한 대기"입니다 ...하지만 여기에서이 디자인에 대해 토론하십시오 – 나는 그것을 바꿀 수 없습니다.
std::atomic_thread_fence(std::memory_order_seq_cst)
에서 완전한 장벽으로 컴파일하지만 전체 개념이 구현 세부 사항이므로 찾을 수는 없습니다. 표준에 대한 언급. (CPU 메모리 모델은 일반적 된다 reorerings 순차적 일관성에 대해 허용되는 용어의 정의이다 예 86 - SEQ CST + 스토어 버퍼 w / 포워딩.)