1. 생산자-소비자(Producer-Consumer) 패턴

작업을 생산 주체와 처리 주체로 나누어 각 생산과 처리의 부하를 조절하는 멀티 스레드 구성 방식.

 

(1) 생산자(Producer)

처리할 일을 받아오는 스레드.

 

(2) 소비자(Consumer)

받은 일을 처리하는 스레드.

 

(3) 코드

mutex m;
queue<int> q;

void Producer()
{
	while (true)
	{
		unique_lock<mutex> lock(m);
		q.push(100);

		// 100 밀리세컨드 동안 대기
		this_thread::sleep_for(100ms);
	}
}

void Consumer()
{
	while (true)
	{
		unique_lock<mutex> lock(m);
		if (q.empty() == false)
		{
			int data = q.front();
			q.pop();
			cout << data << endl;
		}
	}
}


int main()
{
	thread t1(Producer);
	thread t2(Consumer);

	t1.join();
	t2.join();
}

Producer는 100ms 마다 100을 큐에 집어넣고, Consumer는 큐의 맨 앞을 받아와 pop하고 출력하는 코드이다.

위의 두 스레드는 서로 락을 획득하기 전에 대기열에서 대기하기 때문에 값을 출력하기까지 걸리는 시간이 오래 걸린다는 것을 알 수 있다.

 

 

2. 커널 오브젝트(Kernel Object)

운영체제 내에서 관리되는 자원을 나타내는 객체이다.

 

(1) 참조 횟수(Usage Count)

해당 객체를 참조하고 있는 프로세스 혹은 스레드의 수를 나타낸다.

0이 되면 운영체제가 해당 객체를 정리하고 메모리를 확보한다.

 

(2) 시그널(Signal / Non-Signal)

이벤트의 발생을 나타낸다.

대기 중인 다른 스레드에게 이벤트의 발생을 알린다.

또는 이벤트가 발생하는 것을 기다린다.

 

 

3. 이벤트(Event)

커널 오브젝트에 포함되어 있다.

작업이 끝나면(락이 해제되면) 가동중인 스레드가 이벤트를 발생시킨다.

대기중인 스레드가 이벤트를 대기하다가 이벤트가 발생하면 즉시 작업을 시작할 수 있다.

 

(1) 사용

windows.h 헤더를 추가하여 사용한다.

HANDLE hEvent;

int main()
{
	// CreateEvent(보안속성, 수동 리셋 여부, Signal 초기상태, 이름)
	hEvent = ::CreateEvent(NULL, FALSE, FALSE, NULL);
    
    //~~~~~~
    
    ::CloseHandle(hEvent);
}

hEvent는 커널 오브젝트이다.

다른 프로그램과도 상호작용 할 수 있다.

 

(2) 이벤트 발생과 대기

void Producer()
{
	while (true)
	{
		unique_lock<mutex> lock(m);
		q.push(100);

		// 이벤트 설정
		::SetEvent(hEvent);

		// 100 밀리세컨드 동안 대기
		this_thread::sleep_for(100ms);
	}
}

void Consumer()
{
	while (true)
	{
		// 이벤트가 발생할 때까지 무한정 대기
		::WaitForSingleObject(hEvent, INFINITE);

		unique_lock<mutex> lock(m);
		if (q.empty() == false)
		{
			int data = q.front();
			q.pop();
			cout << data << endl;
		}
	}
}

이제 큐에 100을 push하면 이벤트의 Signal이 true가 되면서 hEvent가 발생하기까지 대기하고 있는 Consumer의 코드가 실행된다.

락이 보장되면서 빠른 속도로 작업을 처리할 수 있다는 장점이 있다.

다만 이벤트가 발생할 때까지 지속적으로 검사하기 때문에 CPU 자원이 낭비될 수 있다(비용 소모가 크다).

 

4. 조건 변수(Condition Variable)

커널 오브젝트인 이벤트와 다르게 조건 변수는 유저 오브젝트이다.

 

(1) 코드

condition_variable cv;

void Producer()
{
	while (true)
	{
		unique_lock<mutex> lock(m);
		q.push(100);

		// 대기 상태인 스레드를 하나만 깨움<->notify_all
		cv.notify_one();

		// 100 밀리세컨드 동안 대기
		this_thread::sleep_for(100ms);
	}
}

void Consumer()
{
	while (true)
	{
		unique_lock<mutex> lock(m);
		cv.wait(lock, []() { return q.empty() == false; });

		{
			int data = q.front();
			q.pop();
			cout << data << endl;
		}
	}
}

CreateEvent에 대응하는 notify_one(혹은 notify_all)이다.

대기 상태인 스레드를 하나(혹은 전부) 깨우는 역할을 한다.

이후에 wait 함수를 사용하여 어떤 조건이 충족될 때까지 대기하도록 할 수 있다.

위에서는 큐가 비어있지 않으면(값이 하나라도 있으면) 아래 코드를 실행한다.

 

 

(2) 원리

- 공유 자원에 락을 건다.

void Producer()
{
	while (true)
	{
		unique_lock<mutex> lock(m);

 

 

- 공유 자원의 값을 수정한다.

		q.push(100);

 

- 락을 해제한다.

- condition_variable을 이용하여 notify 한다.

		// 대기 상태인 스레드를 하나만 깨움<->notify_all
		cv.notify_one();

 

- 락을 잡으려고 시도한다(이미 락이 잡혔다면 스킵한다).

- 조건을 확인한다.

void Consumer()
{
	while (true)
	{
		unique_lock<mutex> lock(m);
		cv.wait(lock, []() { return q.empty() == false; });

 

- 조건을 만족했으면 바로 빠져나와 코드를 실행한다.

조건을 만족하지 못했으면 락을 해제하고 대기 상태로 전환한다.

		{
			int data = q.front();
			q.pop();
			cout << data << endl;
		}

 

즉, 락을 해제하는 방법이 포함되어 있기 때문에 커널 오브젝트-이벤트-와 다르게 락을 먼저 선언하고 사용해야 한다.

 

 

(3) 특징

- 이벤트가 바쁜 대기(Busy Waiting)하는 것과 다르게 대기할 때 CPU 자원을 소모하지 않는다.

- 대기중인 스레드가 대기 상태에 들어가기 전에 락을 획득해야 하기 때문에 경쟁 조건이 발생하지 않는다.

- 윈도우가 아닌 환경에서 활용할 수 있다.