ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • C++ Conditional Variable(조건변수)
    C++/C++ 멀티스레드 2022. 10. 14. 12:54
    728x90

    C++표준에서 제공하는 Conditional Variable을 이용하면

    멀티스레드간 동기화를 구현할 수 있습니다.

     

    조건변수를 사용하는 대표적인 패턴인 생산자-소비자 패턴을 예시로 들어보겠습니다.

    #include <iostream>
    #include <thread>
    #include <atomic>
    #include <mutex>
    #include <Windows.h>
    
    queue<int> q;
    mutex m;
    
    void Producer()
    {
    	while (true)
    	{
                {
                    unique_lock<mutex> lock(m);
                    int pushData = rand() % 100;
                    q.push(pushData);
                    cout << pushData << " push" << endl;
                }
    	}
    }
    
    void Consummer()
    {
    	while (true)
    	{
    		if (q.empty() == false)
    		{
                        unique_lock<mutex> lock(m);
                        int data = q.front();
                        q.pop();
                        cout << data << " pop" << endl;
    		}
    	}
    }

     

    • producer는 큐에 데이터를 추가한다.
    • consumer는 큐에서 데이터를 꺼내쓴다.

    이 경우 문제점은

    큐에 데이터가 없을 때, Consumer 스레드는 while문을 무한반복하며, 계속 큐에 데이터가 있는지 검사한다.

    입니다.

     

    이 문제를 해결하기 위해 저번 포스팅에서는 "이벤트"를 이용했습니다.

     

    C++ 멀티스레드 동기화, 이벤트 사용법

     

    이벤트와 동일한 기능을 Conditional Variable로 구현할 수 있습니다.

     


    Conditional Varialbe과 관련된 주요 함수들을 살펴보고 가겠습니다.

     

    wait(unique_lock<mutex>& lk)
    • wait을 호출하는 스레드는 lk에 대한 락을 걸고 있어야합니다.
    • wait을 호출하면, unlock을 호출해서, wait을 호출한 스레드를 블락(멈춰!)시기고,  알림이 오기를 기다립니다.
    • notify_one() 혹은 notify_all()을 통해서 wait이 해제되면, lk.lock()을 다시 호출하고, 락을 획득하면 wait함수가 종료되고 아래의 코드들을 차례로 실행하게 됩니다.
      • lock_guard가 아닌 unique_lock을 사용하는 이유는 여기에 있습니다.
      • lock_guard는 락을 걸면 소멸되기 전까지 임의로 lock, unlock을 할 수 없지만, unique_lock은 가능하기때문입니다.
    • wait을 호출할 때, 대기할 시간을 설정할 수도 있고, predicate을 추가해서, wait이 해제될 때 검사할 조건을 추가할 수도 있습니다.
    notify_one()
    notify_all()
    • 말그대로입니다. 조건변수를 기다리고있는(wait) 스레드를 하나만(one) 깨우거나, 모두(all) 깨웁니다.

    이제 조건변수를 이용해서 코드를 작성해 보겠습니다.

     

    queue<int> q;
    mutex m;
    condition_variable cv;
    
    void Producer()
    {
    	while (true)
    	{
    		{
    			unique_lock<mutex> lock(m);
    			int pushData = rand() % 100;
    			q.push(pushData);
    			cout << pushData << " push" << endl;
    		}
    		cv.notify_one();
    	}
    }
    
    void Consummer()
    {
    	while (true)
    	{
    		unique_lock<mutex> lock(m);
    		cv.wait(lock, []() {return !q.empty(); });
    		int data = q.front();
    		q.pop();
    		cout << data << " pop" << endl;
    	}
    }
    
    int main()
    {
    	std::thread t1(Producer);
    	std::thread t2(Consummer);
    
    	t1.join();
    	t2.join();
    }

     

    • producer에서는 데이터를 넣어준 후에 조건변수에 notify_one을 해줌으로써, 해당 조건변수를 기다리는 스레드에게 알려줍니다.
    • consumer에서는 wait을 호출하여, 알림이 올 때까지 기다립니다.
      • 여기서는 predicate을 사용했습니다.
      • 알림이 왔을 때 predicate을 만족하면 wait을 끝내고 일어납니다.
      • 여기서는 큐가 비어있지 않다면 wait을 중단하고 깨어납니다.

    spurious wakeup

    간단히 비정상적으로 깨어났다! 라는 의미입니다.

     

    wait 상태에 있던 스레드는 다른 스레드의 notify를 통해서 깨어나야 하지만,

    OS issue로 인해서, 이유없이 깨어나는 경우가 있습니다.

     

    그럴 때는 위의 코드에서와 같이 predicate으로 조건을 검사해서

    깨어난 스레드가 작업을 할 수 있는 상황인지 확인해야하는거죠.

     

    위의 코드에서는 큐가 비어있지 않다면 true를 반환(!q.empty())하여 작업할 수 있는 상황이 맞다는 것을 인지합니다.

     

    추가로 OS issue가 아니더라도,

    consumer 스레드가 여러개 있을 수 있습니다.

     

    그리고, notify_all()을 통해서 조건변수를 기다리는 모든 consumer 스레드를 깨웠는데,

    그 중 가장 빠른 consumer 스레드가 이미 queue의 데이터를 다 빼서 써버렸을 수 있습니다.

     

    이러면 다른 consumer 스레드는 작업을 할 수 없는 상황이 됩니다.

    작업을 할 수 없는 상황이면 다시 lock을 풀고, wait상태로 돌아갑니다.

     

    이럴 때에도 predicate을 통해서 큐에 작업이 있는지 확인할 수 있기에 predicate이 도움이 됩니다.

    728x90

    'C++ > C++ 멀티스레드' 카테고리의 다른 글

    TLS(Thread Local Storage)  (0) 2022.11.08
    C++ future(promise, async, packaged_task)  (2) 2022.10.15
    C++ 멀티스레드 동기화, 이벤트 사용법  (0) 2022.10.12
    C++, Sleep을 이용한 Lock구현  (0) 2022.10.10
    C++ SpinLock구현  (0) 2022.10.09

    댓글

Designed by Tistory.