condition_variable 은 전통적인 CreateEvent, SetEvet 를 대체하는 방식으로 볼 수 있다
condition_variable 은 멀티 플랫폼에서도 작동하는 표준으로 들어가 있다
condition_variable 은 커널오브젝트인 event 와 유사한것 같지만
유저모드 레벨 오브젝라는 점이 다르다
즉, 동일한 프로그램 내에서만 사용 가능하다
표준 mutex 와 짝지어 사용 가능하다
condition_variable cv;
#include "pch.h"
#include <iostream>
#include "CorePch.h"
#include <thread>
#include <atomic> //멀티 플랫폼에서 작동 가능
#include <vector>
#include <mutex>
#include "AccountManager.h"
#include "UserManager.h"
#include <chrono>
#include <queue>
#include "windows.h"
using namespace std;
mutex m;
queue<int32> q;
//HANDLE handle;
//condition_variable 은 커널오브젝트인 event 와 유사한것 같지만
//유저모드 레벨 오브젝라는 점이 다르다 즉, 동일한 프로그램 내에서만 사용 가능하다
//표준 mutex 와 짝지어 사용 가능하다
condition_variable cv;
void producer()
{
while (true)
{
// 1) lock 을 잡고
//2 공유변수 값 수정 q
//3 lock 을 풀고
//4 조건 변수 통해 다른 스레드에게 통지 (condition variable 로 통지
{
unique_lock<mutex> lock(m); //1. lock 을 잡는다
q.push(100); //2. 공유 값 수정
} //3. lock 을 풀어줌
//::SetEvent(handle); //event 를 signal 상태로 만든다
cv.notify_one(); //SetEvent 대신 notify 를 해줘서 wait 중인 스레드들 중 딱 1개를 깨운다
//this_thread::sleep_for(100ms);
}
}
//q 에 데이터가 있을 때에만 pop 하여 꺼내서 출력하고 q 에 데이터가 없다면 대기하게 된다
void consumer()
{
while (true)
{
//::WaitForSingleObject(handle, INFINITE); //SetEvent 사용시 wait for 이함수로 대기했지만...
//std::cout << "ConSumer 함수" << std::endl;
{
//1) 우선 unique_lock 에서 lock(m) 락을 잡으려고 시도한다
//락이 이때 안잡혀 있으면 무조건 잡으려고 시도한다, 이미 잡혔으면
//unique_lock 단계는 통과 되고 wait 족으로 가려고 한다
//2) 조건 확인
// 1=>조건 만족하면 바로 아래 코드를 이어서 진행한다
// 2=>조건 만족하지 않는다면 lock 을 풀어주고 대기상태로 전환한다
unique_lock<mutex> lock(m); //우선 lock 을 해서 스레드를 한번 block 해줌 (push 나 pop 중 하나만 일어나도록 하기 위함)
//notify_one 에 의해서 위에 것이 꺠어나서 이쪽으로왔지만 만약
//다른 스레드에서 락을 획득한다면 아래 조건이 없다면 문제가 생긴다, 이 상황을 가짜 기상이라하고 이걸 방지하기 위해 아래 wait 가 있는것
//notify_one 할때는 가짜 기상을 염두해두어야 하고 그래서 조건을 다시 한번 체크 하게 된다, 상태가 다른곳에서 변하게 되어졌을수 있기 때문에
cv.wait(lock, []() { return q.empty() == false; }); //조건을 넣어주는데, 조건이 참일 때까지 기다리게 한다
//q가 비어 있으면 결과는 (true == false) => false 인데 wait(false) 로 들어오면 lock 풀고 대기 한다, 즉 스레드가 중지된다
//이렇게 lock 을 풀어줘야 하는 상황이 발생 할 수 있기 때문에 unique_lock 을 써야만 한다, conditional_variable 을 사용할때는
//q가 비어 있지 않으면 결과는 (false == false) => true wait(true) 로 들어오면 빠져나와서 다음 코드를 진행한다
//wait 에 false 가 들어가면 lock풀고 대기
//wait 에 true 가 들어가면 다음 코드 진행
//q가 비어 있다면 => (true==false) => false , wait(false) =>결과적으로 lock 풀고 대기 한다
//q가 차 있다면 => (false==false) => true , wait(true) => 결과적으로 다음 실행해서 데이터 꺼내서 출력
//조건이 맞으면 다음으로 넘어가고 그렇지 않으면 lock을 풀어주고 대기 상태로 전환된다
//lock 을 또 다시 wait 하는 이유는 Spurious wakeup(가짜 기상) 때문인데
//unique_lock<mutex> lock(m); 해서 대기하고 있는 상황에서 다른 스레드에서 notify_one를해서 쓰레드를 깨웠는데
//unique_lock<mutex> lock(m); 과 cv.wait(lock, []() { return q.empty() == false; }); 사이에서 다른곳에서 lock 을 할 수도 있기 때문에
//wait 에서 lock 을 한번더 확인해 주는것인데 그 사이에 다른 스레드가 lock 해버리는 상황을 Spurious wakeup 이라 한다
//condition variable 에서 wait 하는 순간 발생 하는 것들
//1 : lock 을 잡고 ( 만약 wait 위에서 lock 을 잡았다면 wait 에서 다시 lock 을 잡진 않는다
//2 : 조건 확인
//3-1 : 조건 만족 하면 빠젼 나와서 이어서 코드를 진행한다
//3-2 : 조건 만족하지 않으면 lock 을 풀어주고 대기 상태로 전환한다 (=> 경우에 따라서 constant variable 이 unlock 을 해야 하기 대문에 unique_lock 을 사용해야만 함)
//3-2-sub : 3-2 에서 대기 하고 있다가 다른곳에서 notify 를 호출해주면 그때 다시 1번 부터 확인하여 실행 여부가 결정된다
if (q.empty() == false)
{
int32 data = q.front();
q.pop();
cout << data <<"\t"<< q.size() << std::endl;
}
}
this_thread::sleep_for(100ms);
}
}
int main()
{
//커널오브젝트
//Usage Count
//이벤트 속성 : NULL
//auto rest 방식 으로 지정
//초기 값
//이름은 null
//Event 는 유저모드가 아닌 커널모드에서 관리 되는것이기 떄문에 커널단에서 처리된다
//handle = ::CreateEvent(nullptr, true, false, nullptr);
thread t1(producer);
thread t2(consumer);
t1.join();
t2.join();
//::CloseHandle(handle);
return 0;
}
produce 에선 (데이터를 추가해주는 쪽)에선 비교적 간단하게 다음 처럼 처리해 준다
Conditional_variable 의 처리 순서
1) lock 을 잡고
2 공유변수 값 수정 q
3 lock 을 풀고
4 조건 변수 통해 다른 스레드에게 통지 (condition variable 로 통지
이런 프로세스로 추가해주고
Produce 예시를 적용해보자면 이렇게 된다
{
unique_lock<mutex> lock(m); //1. lock 을 잡는다
q.push(100); //2. 공유 값 수정
} //3. lock 을 풀어줌
4. 대기하고 있는 스레드 중에서 스레드들 중 딱 1개만 깨운다
cv.notify_one();
데이터 추가할때 다른 곳에서 쓰는것과 충돌나는 일을 막기 위해 mutex 로 묶어준다
이후 처리가 끝나면 그냥단순 통지만 하고 끝
consumer 에선 다음 처리 처리한다
unique_lock<mutex> lock(m); //우선 lock 을 해서 스레드를 한번 block 해줌 (push 나 pop 중 하나만 일어나도록 하기 위함)
cv.wait(lock, []() { return q.empty() == false; }); //조건을 넣어주는데, 조건이 참일 때까지 기다리게 한다
Condition variable 은 다음 특징이 있다
wait(false) 에 false 가 들어가면 lock풀고 대기
wait(true) 에 true 가 들어가면 다음 코드 진행
q가 비어 있다면 => (true==false) => false , wait(false) =>결과적으로 lock 풀고 대기 한다
q가 차 있다면 => (false==false) => true , wait(true) => 결과적으로 다음 실행해서 데이터 꺼내서 출력
Spurious wakeup
lock 을 또 다시 wait 하는 이유는 Spurious wakeup(가짜 기상) 때문인데
unique_lock<mutex> lock(m); 해서 대기하고 있는 상황에서 다른 스레드에서 notify_one를해서 쓰레드를 깨웠는데
unique_lock<mutex> lock(m); 과 cv.wait(lock, []() { return q.empty() == false; }); 사이에서 다른곳에서 lock 을 할 수도 있기 때문에
wait 에서 lock 을 한번더 확인해 주는것(즉 lock 을 한번 더 건다)인데 하지만 그 사이에 다른 스레드가 lock 해버리는 상황이 발생 되면 wait 은 다른곳이 lock 을 했기 때문에 대기 해야 하고 이 것을 Spurious wakeup 즉 가짜 wakeup 이라 한다
다시 정리 하자면 이중으로 unique_lock 에 이어 한번더 wait 에서 lock 을 체크 하는데
unique_lock 과 wait 사이에서 다른 스레드가 lock 을 가져 갈수 있기 때문에 방지 차원에서 wati 에서 lock 을
체크하는 로직이 들어가게 된것이다
Spurious wakeup
A spurious wakeup happens when a thread wakes up from waiting on a condition variable that's been signaled, only to discover that the condition it was waiting for isn't satisfied. It's called spurious because the thread has seemingly been awakened for no reason. But spurious wakeups don't happen for no reason: they usually happen because, in between the time when the condition variable was signaled and when the waiting thread finally ran, another thread ran and changed the condition. There was a race condition between the threads, with the typical result that sometimes, the thread waking up on the condition variable runs first, winning the race, and sometimes it runs second, losing the race.
On many systems, especially multiprocessor systems, the problem of spurious wakeups is exacerbated because if there are several threads waiting on the condition variable when it's signaled, the system may decide to wake them all up, treating every signal( ) to wake one thread as a broadcast( ) to wake all of them, thus breaking any possibly expected 1:1 relationship between signals and wakeups.[1] If there are ten threads waiting, only one will win and the other nine will experience spurious wakeups.
To allow for implementation flexibility in dealing with error conditions and races inside the operating system, condition variables may also be allowed to return from a wait even if not signaled, though it is not clear how many implementations actually do that. In the Solaris implementation of condition variables, a spurious wakeup may occur without the condition being signaled if the process is signaled; the wait system call aborts and returns EINTR.[2] The Linux p-thread implementation of condition variables guarantees it will not do that.[3][4]
Because spurious wakeups can happen whenever there's a race and possibly even in the absence of a race or a signal, when a thread wakes on a condition variable, it should always check that the condition it sought is satisfied. If it is not, it should go back to sleeping on the condition variable, waiting for another opportunity.
가짜 wakeup 로 인해 성능이 악화 되는 상황
가짜 wakeup 의 상황이 되면 현재 돌아가려고 하는 스레드가 우선권을 race 에서 뺐기고 다른 스레드가 계속 가져가게 되서 느려지는데 만약 실행되야 하는 스레드가 있고 다른 스레드 10개가 돌아가고 있다면 이때 레이스를 하면서 실행되야 하는 스레드는 더욱더 지연 될 수 있게 되어 성능이 악화 될 수 있다
'운영체제 & 병렬처리 > Multithread' 카테고리의 다른 글
future , async, promise, packaged_task (2) (0) | 2022.09.11 |
---|---|
future , async (간략한 비동기,동기 함수 실행) (1) (0) | 2022.09.11 |
Conditional Variable - basic (0) | 2022.09.10 |
Event (이벤트) 순서 제어, WaitForSingleObject (0) | 2022.09.09 |
Sleep 함수의 이해 (0) | 2022.09.09 |