#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

 

condition_variable 예제 (Produce, Consumer)

condition_variable 은 전통적인 CreateEvent, SetEvet 를 대체하는 방식으로 볼 수 있다 condition_variable 은 멀티 플랫폼에서도 작동하는 표준으로 들어가 있다 condition_variable 은 커널오브젝트인 event..

3dmpengines.tistory.com

 

반응형

+ Recent posts