condition variable (조건 변수)를 사용하면 c++에서 멀티스레드 간 동기화를 구현할 수 있다
그렇지만 condition variable을 사용하는 것보다는,
packaged task나 async와 같이 c++에서 제공하는 task를 이용하는 것이
멀티스레드 간 동기화를 안정적으로 구현하는 방법이라는 점을 명심하자
2021.08.04 - [c++/library] - [c++] thread vs task (thread 와 async)
2020/12/15 - [c++ language/library] - [c++] future
2020/12/15 - [c++ language/library] - [c++] packaged task
2020/12/15 - [c++ language/library] - [c++] async
아래 해외 블로그에서 조건 변수를 사용하여 스레드간 동기화를 구현하는 간단한 예제와
conditional variable의 문제점을 설명하고 있으니 참고
condition variable (조건 변수)는 notification이라는 개념을 통해 스레드간의 동기화를 지원하고 있다
우리는 바로 이 notification을 이용해서
sender/receiver 또는 producer/consumer와 같은 병렬 스레드의 실행 흐름을 제어할 수 있는 것이다
예를들어 아래와 같이 동작하는 sender, receiver를 구현할 수 있다
- receiver는 notification을 받을 때까지 작업을 대기하고 있다가
- sender가 notification을 보내면 작업을 수행한다
std::condition_variable
멀티 스레드 환경에서 동기화를 수행하려면
sender와 receiver 둘 다 condition variable의 인스턴스를 가지고 있어야 한다 (동일한 condition variable을 공유해야 함)
코드 구현은 단순한데
- receiver는 wait 함수를 호출해서 notification을 기다리고 (condVar.wail(lck))
- sender는 notify 함수를 호출해 receiver에게 notification을 전달한다 (condVar.notify_one())
사실 위에 설명한 내용이 condition variable을 사용하는 이유의 전부라고 할 수 있다
#include <iostream>
#include <condition_variable>
#include <mutex>
#include <thread>
std::mutex mutex_;
std::condition_variable condVar;
void doTheWork(){
std::cout << "Processing shared data." << std::endl;
}
void waitingForWork(){
std::cout << "Worker: Waiting for work." << std::endl;
std::unique_lock<std::mutex> lck(mutex_);
condVar.wait(lck);
doTheWork();
std::cout << "Work done." << std::endl;
}
void setDataReady(){
std::cout << "Sender: Data is ready." << std::endl;
condVar.notify_one();
}
int main(){
std::cout << std::endl;
std::thread t1(waitingForWork);
std::thread t2(setDataReady);
t1.join();
t2.join();
std::cout << std::endl;
}
위 코드를 조금 더 설명하면 먼저 main 함수에서 t1과 t2 두 개의 스레드를 생성한다
각 스레드는 각각 호출가능한 함수인 waitingForWork와 setDataReady를 각각 인자로 받아 병렬로 실행된다
- waitingForWork
- lock을 유지한 상태로 notification을 기다리고 (condVar.wait(lck))
- notification을 수신하면 doTheWork()를 수행
- setDataReady
- 준비가 완료되면 notify_one() 함수를 호출
코드 실행결과는 사실 특별하지 않고 생각했던 그대로 실행될 것이다
Worker: Waiting for work # Receiver가 기다리다가 Sender: Data is ready. # Sender가 준비되면 Processing shared data. # Notification을 전달하고 Work done. # Receiver가 작업을 완료 |
그런데 한 가지 짚고 가야할 것이 있는데.. 🙄
Spurious wakeup
악마는 디테일에 있다고 한다
바로 condition variable이 spurious wakeup라는 문제를 내재하고 있다는 것이다
Spurious wakeup
POSIX나 모든 OS에서 signal을 줬을때 하나만 깨어나는 것이 아니라 동시에 여러 wait condition이 깨어나는 현상을 뜻합니다. 이는 OS의 성능 이슈이기 때문에 개발자 영역으로 남겨져 있습니다.
위와 같은 피치 못할 문제로
sender가 notification을 다 보내기도전에 receiver가 대기하지 않고 작업을 수행해버리는 불상사가 일어날 수 있다
결국 이러한 문제를 막으려면 개발자가 개입해야 한다
바로 wait 함수에 predicate를 추가하는 것이다 (predicate는 true 또는 false를 반환하는 callable이어야 함)
이전 코드와 달라진 것은 dataReady라는 boolean값을 사용한 것 말고는 없다
대신 이 dataReady 값을 체크해서 sender가 정상적으로 전달한 신호인지를 더블-체크하는 방법을 사용할 수 있다
#include <iostream>
#include <condition_variable>
#include <mutex>
#include <thread>
std::mutex mutex_;
std::condition_variable condVar;
bool dataReady;
void doTheWork(){
std::cout << "Processing shared data." << std::endl;
}
void waitingForWork(){
std::cout << "Worker: Waiting for work." << std::endl;
std::unique_lock<std::mutex> lck(mutex_);
condVar.wait(lck,[]{return dataReady;});
doTheWork();
std::cout << "Work done." << std::endl;
}
void setDataReady(){
std::lock_guard<std::mutex> lck(mutex_);
dataReady=true;
std::cout << "Sender: Data is ready." << std::endl;
condVar.notify_one();
}
int main(){
std::cout << std::endl;
std::thread t1(waitingForWork);
std::thread t2(setDataReady);
t1.join();
t2.join();
std::cout << std::endl;
}
unique_lock과 lock_guard를 따로 사용한 이유?
- receiver(waitingForWork)에서는 condition variable 내에서 mutex의 unlock과 lock을 을 여러번 호출하기 때문에 unique lock을 사용
- sender(setDataReady)에서는 mutex의 lock과 unlock이 단 한 번씩만 수행되므로 lock guard로도 충분
cf. unique_lock과 lock_guard의 차이는 다음 페이지 참조:
2020/12/16 - [c++ language/library] - [c++] mutex, lock guard, unique lock, shared mutex
Lost wakeup
아 그럼 이제 다 됐나? (아니다)
condition variable의 문제는 아직도 남아있다
첫 번째 코드를 한 10번정도 실행하면 프로그램이 가끔 멈추는데
(앞서 conditional variable 보다 async와 같은 task를 사용해야 한다고 말했던 이유임)
멈추는 문제가 발생한 이유는
receiver가 wait 함수를 호출하기도 전에 sender가 notification을 전달해버리는 경우가 발생하기 때문이다
그러면 notification은 소실되고 receiver는 영원한 대기상태에 빠진다
그럼 이 문제를 해결하는 방법은 무엇일까?
condition variable을 통해 spurious wakeup, lost wakeup의 모든 문제를 피하기는 어렵다
물론 predicate를 사용하는 것만으로도 문제를 해결하는데 도움이 될 수 있겠지만
결국 c++에서 멀티스레딩을 잘 사용하려면 task를 이용할 수 있어야 한다
Thread와 Task?
2021.08.04 - [c++/library] - [c++] thread vs task (thread 와 async)
'💻 programming > c++' 카테고리의 다른 글
[c++] thread (0) | 2021.08.04 |
---|---|
[c++] lvalue reference and rvalue reference (0) | 2021.08.04 |
[c++] std::shared_mutex (0) | 2020.12.16 |
[c++] std::timed_mutex (0) | 2020.12.16 |
[c++] std::recursive_mutex (std::mutex 비교) (0) | 2020.12.16 |
댓글