(작성 중)Spurious wakeup, lost wakeup & Spurious Failure
C++/C++ 2021. 10. 4. 21:02 |Spurious wakeups
condition_variable을 사용할 때 모종의 환경/이유*로
notify가 호출되지 않았음에도 불구하고 쓰레드가 깨어나는 현상이 있다.
이를 spurious wakeups 라 불리는데, 어떤 분명한 이유없이 쓰레드가 깨어난다.
std::mutex mutex_;
std::condition_variable condVar;
void waitingForWork(){
std::cout << "Waiting " << std::endl;
std::unique_lock<std::mutex> lck(mutex_);
condVar.wait(lck); // (1)
std::cout << "Running " << std::endl;
}
void setDataReady(){
std::cout << "Data prepared" << std::endl;
condVar.notify_one(); // (2)
}
int main(){
std::cout << std::endl;
std::thread t1(waitingForWork);
std::thread t2(setDataReady);
t1.join();
t2.join();
std::cout << std::endl;
}
만일 위 코드에서 spurious wakeup 이 발생한다면,
필요할 때 호출을 통해 깨어났어야 할 waiting 쓰레드가
제멋대로 깨어나 먼저 동작을 재개하는 현상이 발생할 수 있다.
이는 어플리케이션에 심각한 문제가 될 수 있다.
이런 문제를 막기 위해 신호를 저장하는 멤버변수에 대한 조건 체크를 해야한다.
조건 체크는 단순하게 if를 사용하여서는 안되고, while 루프에서 수행하여야 한다.
위 코드의 (1)부분을 아래와 같이 바꾸면 된다.
std::unique_lock<std::mutex> lck(mutex_);
while ( ![]{ return dataReady; }() {
// time window (1)
condVar.wait(lck);
}
dataReady변수의 참 거짓 여부에 따라 wait 상태에 들어가거나,
wait상태로 들어가지 않고 다음 로직을 이어 나갈지를 결정하게 된다.
위 코드는 아래와 같이 짧게 줄일 수 있다.
condVar.wait(lck, []{ return dataReady; });
* 모종의 환경/이유* : POSIX Thread나 Windows API를 사용하는 경우 발생할 수 있다.
Lost Wakeup
Receiver가 wait 상태가 되기도 전에 Sender가 notification을 보내버리는 경우를 생각해보자.
Receiver는 wait상태가 아직 되지도 않았는데 wakeup 시그널을 받게 될 경우 wakeup을 무시해버리게된다.
이 상황을 Lost Wakeup이라고 한다.
Lost Wakeup이 발생한 뒤 Receiver가 wait명령을 만나서 wait상태에 접어들었다고 가정해보자.
그런데 더 이상 Sender로부터 notification이 오지 않고 있다.
이 때 Sender는 Receiver쪽에서 어떤 작업을 수행해주기를 기다리고 있다면
서로 교착상태처럼 무한 대기에 빠질 수도 있다.
Spurious Failure
atomic계열의 CAS(Compare And Swap)연산을 할 때
명확하지 않은 이유로 인해 CAS작업이 실패하거나 무시되는 경우가 발생하기도 한다.
이를 Spurious Failure 라고 한다.
C++을 예로 들면 CAS연산 관련 함수 중에서 spurious failure가 발생하더라도
CAS연산이 정상 완료될 때까지 재시도하여 정상 동작을 보장해주는 함수가 있고,
그렇지 않고 spurious failure를 허용하는 함수가 있다.
그런데 문제는 spurious failure를 허용하는 함수를 사용하게 되면
CAS가 spurious failure에 의해서 실패할 하더라도 상관이 없도록 로직을 짜야한다.
왜냐하면 CAS연산의 비교 원본 대상(*this)과 인자(expected)의 일치 유무에 관계없이
expected인자와 *this가 일치하지 않는 상황처럼 동작하기 때문이다.
즉, spurious failure가 발생하면
expected인자가 *this와 일치하더라도
expected인자를 *this로 변경 시킨뒤 CAS함수가 false를 반환해버리는 동작을 하게 된다.
따라서 루프 구조 + weak계열 CAS를 사용한다.
대부부의 경우 이 구조가 성능이 좋다.
로직상의 제약에 의해 weak계열 CAS를 사용할 수 없다면
spurious failure에 의한 CAS연산 실패를 방지하는 strong계열의 CAS 함수를 사용해야한다.
strong계열 강한 CAS 조작 명령을 통해 CAS함수가 동작하도록 한다.
그렇기 때문에 strong계열의 CAS함수는 오버헤드가 더 높지만 확실한 동작을 보장한다.
반면 weak계열의 CAS함수는 오버헤드는 낮지만,
CAS함수가 동작하지 않고 false를 반환하더라도 문제가 없는 로직에서 사용한다면 효율이 좋다.
CAS가 실패하는 이유는?
CPU 설계상 CAS를 요청헀을 때 메모리 베어러 등 여러가지 이유로 CAS를 수행하기 적합하지 않다고 판단한다고 한다
(확실하지 않음...)
'C++ > C++' 카테고리의 다른 글
메모리 오염( Memory Stomping )방지를 위한 Allocator (작성 중) (0) | 2021.10.15 |
---|---|
가상 소멸자 (0) | 2021.10.11 |
Spin Lock 구현해보기 (0) | 2021.10.03 |
volatile 키워드 (0) | 2021.10.03 |
파일 읽기/쓰기 & Regex - 정확히 일치하는 단어 찾기 (0) | 2021.10.01 |