#include <iostream>
#include <mutex>
#include <queue>
#include <windows.h>
using namespace std;
mutex m;
queue<__int32> q;
HANDLE handle;
void producer()
{
while (true)
{
{
unique_lock<mutex> lock(m);
q.push(100);
}
::SetEvent(handle);
this_thread::sleep_for(1000ms);
}
}
void consumer()
{
while (true)
{
::WaitForSingleObject(handle, INFINITE);
cout << "consu" << endl;
/*
unique_lock<mutex> lock(m);
if (q.empty() == false)
{
__int32 data = q.front();
q.pop();
cout << q.size() << endl;
}
*/
}
}
int main()
{
handle = ::CreateEvent(NULL, FALSE, FALSE, NULL);
thread t1(producer);
thread t2(consumer);
t1.join();
t2.join();
return 0;
}
위 코드는 1초마다 "consu" 가 뜨는 로직이다
여기서 주목할 것은 ::SetEvent(handle); 를 통해서 ::WaitForSingleObject(handle, INFINITE); 이것 또한 깨어난다는 것이다
그리고 다음로직을 보면 q 에 추가된 개수가 1개 추가되고 1개 소비하는 것이 아닌 계속 증가하는 것을 볼수 있다
void producer()
{
while (true)
{
{
unique_lock<mutex> lock(m);
q.push(100);
}
::SetEvent(handle);
//this_thread::sleep_for(1000ms);
}
}
void consumer()
{
while (true)
{
::WaitForSingleObject(handle, INFINITE);
unique_lock<mutex> lock(m);
if (q.empty() == false)
{
__int32 data = q.front();
q.pop();
cout << q.size() << endl;
}
}
}
문제는
::WaitForSingleObject(handle, INFINITE);
----- 절취 ------
unique_lock<mutex> lock(m);
----- 절취 ------ 사이에 producer 스레드가 돌아가서 lock 을 먼저 잡을수 있고 q 에 데이터를 먼저 추가 할수 있기 때문에 개수가 증가하는 문제가 발생하는 것
bool expected = false;
bool desired = true;
flag.compare_exchange_weak(expected, desired);
compare_exchange_weak 동작은 compare_exchange_strong 과 같은데 spruious failure 가 발생 할 수 있다
즉 이 함수 내(compare_exchange_weak )에서 다른 스레드의 Interruption 을 받아서 값 변경이 중간에 실패 할 수 있다
비교 당시 flag. 와 expected 가 같았지만 내부 처리상황으로 인해(spurious failure) return false 가 리턴 될 수 있다
그래서 이때 wait 함수 안에 lock 을 소유하고 있는지 한번더 판단해주는 동작이 들어가 있어서 이걸 lock 이 다른 스레드에 의해서 소유됐는지 아닌지를 판단 할 수 가 있게 된다
compare_exchange_weak 이게 기본적인 형태인데 만약에 이렇게 실패가 일어나면
프로그래머가 작성해 놓은 while에 compare_exchange_strong 를 놓아서 계속 돌려
성공할때까지 시도할 수 있다 이때 쓰는 것이 compare_exchange_strong 이 된다
compare_exchange_weak 도 사용할떄는 while 루프를 사용해서 처리하는건 마찬가지이다
그런데 compare_exchange_strong 그래서 이게 좀 더 부하가 있을 수 있다곤 하지만
성능 차이가 엄청 크게 차이 나진 않는다
Spurious wakeup(가짜 기상)
//condition_variable 은 커널오브젝트인 event 와 유사한것 같지만
//유저모드 레벨 오브젝라는 점이 다르다 즉, 동일한 프로그램 내에서만 사용 가능하다
//표준 mutex 와 짝지어 사용 가능하다
condition_variable cv;
q가 비어 있으면 결과는 (true == false) => false 인데 wait(false) 로 들어오면 lock 풀고 대기 한다 q가 비어 있지 않으면 결과는 (false == false) => true wait(true) 로 들어오면 빠져나와서 다음 코드를 진행한다 wait 에 false 가 들어가면 lock풀고 대기 wait 에 true 가 들어가면 다음 코드 진행 q가 비어 있다면 => (true==false) => false , wait(false) =>결과적으로 lock 풀고 대기 한다 q가 차 있다면 => (false==false) => true , wait(true) => 결과적으로 다음 실행해서 데이터 꺼내서 출력 |
unique_lock<mutex> lock(m); //현재 스레드가 이구문을 먼저 들어오게 되어 lock을 하게 되면 다른 스레드는 풀릴때까지 대기
//이 라인에선 lock 이 잡힌상태
cv.wait(lock, []() { return q.empty() == false; });
wait 에 대한 케이스를 보면 다음과 같다
case 1 : 결과적으로 return 하는 값이 true 이면 현재 스레드 대기 즉 락을 풀고 현재 스레드는 멈춘다
case 2 : 결과적으로 return 이 false 를 리턴하면 위에서 잡혀 있던 lock 을 그대로 유지한체 아래 구문을 계속 수행한다
그런데 case 1 : 에서 결과적으로 return 하는 값이 true 일때 락을 풀고 현재 스레드는 대기 했었으니
위에서 notice_one 을 하게 되면 case 1: 일때 로직이 아직 wait( 이있는 로직 라인이 실행순서임으로 wait 라인을 통과 할지 말지 즉 다시 lock 을 잡을지 아니면 다시 lock 을 풀고 현재 스레드를 대기 시킬지를 결정하게 된다
가짜기상의 가능성 : 위에서 cv.notify_one(); 를 했으면 무조건 조건식이 만족하는것이 아닌가? 라는 생각을 할수 있지만
cv.notify_one(); 가 실행되고 난 이후 cv.wait(lock, []() { return q.empty() == false; }); 이 구문 안에서 lock 을 소유하기 전에
다른 스레드에서 데이터를 q 에서 꺼낼 수가 있기 때문에 이럴땐 cv.notify_one(); 한다고 해서
항상 []() { return q.empty() == false; } 이 조건을 만족하게 되진 않는다는 것이 가짜기상이라 한다
그래서 notify_one 할때 즉 가짜 기상에 의해서 무조건 wait 구문에서 lock을 잡게 되는 것이 아니라는 것이다
=> 못잡게 될 가능성도 생각해야 한다
그래서 이 가짜 기상을 방지하기 위해서 wait 에다가 조건도(람다) 같이 넣어줘서
가짜 기상이일어나 다른 곳에서 q 의 데이터를 먼저 다 제거 한후라면 cunsumer 에서의 wait 은 실행되면 안됨으로 조건을 추가하여 다시 스레드가 대기하도록 하여 가짜 기상을 방지해 해놓은것
정리하자면 : 꺠어나긴 했는데 다른 스레드에 의해 데이터가 삭제되서 현재 스레드가 실행되지 않고(가짜기상) 조건문에 의해서 또 다시 처리를 하게 되는 것 또한 wait 조건문 이 자체가 가짜 기상에 대한 오류를 방지하는 연결상황이 된다
mutex m;
queue<__int32> q;
HANDLE handle;
condition_variable cv;
void producer()
{
while (true)
{
{
unique_lock<mutex> lock(m);
q.push(100);
}
//wait 중인 스레드가 있다면 1개만 깨운다
cv.notify_one();
}
}
void consumer()
{
while (true)
{
unique_lock<mutex> lock(m); //현재 스레드가 이구문을 먼저 들어오게 되어 lock을 하게 되면 다른 스레드는 풀릴때까지 대기
//이 라인에선 lock 이 잡힌상태
cv.wait(lock, []() { return q.empty() == false; });
if (q.empty() == false)
{
__int32 data = q.front();
q.pop();
cout << q.size() << endl;
}
}
}
int main()
{
handle = ::CreateEvent(NULL, FALSE, FALSE, NULL);
thread t1(producer);
thread t2(consumer);
t1.join();
t2.join();
return 0;
}
https://3dmpengines.tistory.com/2187
'운영체제 & 병렬처리 > Multithread' 카테고리의 다른 글
atomic 가시성, 원자성 (0) | 2022.09.24 |
---|---|
lock-free stack (3) : reference count 로 구현 (0) | 2022.09.24 |
shared_ptr 로 구현한 lock-free 은 lock-free 가 아니다 (3) (0) | 2022.09.22 |
Shared_ptr 와 atomic (0) | 2022.09.22 |
LockFree - Stack (2) - 삭제 처리 (0) | 2022.09.21 |