(C++ : IOCP-3-4) Lock 구현(Condition Variable)

Posted by : at

Category : Cpp   iocp


Event 방식의 문제점

#include <windows.h>

mutex m;
queue<int32> q;
HANDLE handle;

void Producer()
{
    while(ture)
    {
        {
            unique_lock<mutex> lock(m);
            q.push(100);
        }
        ::SetEvent(handle);
    }
}

void Consumer()
{
    while(true)
    {
        // 여기서 push가 들어가고

        ::WaitForSingleObject(handle, INFINITE);

        // 여기서 또 push가 들어갈 수 있음

        /* 
            결론만 말하자면 Event를 기다리는 부분과 
            lock을 잡는 부분이 atomic하지 못하니
            그 사이에서 multi-thread에 safe하지 못하다
        */

        unique_lock<mutex> lock(m);
        if(q.empty() == false)
        {
            int32 data = q.front();
            q.pop();

            // << 여기부터 읽으시오 >>
            // 여기서 size를 조사하면,
            // push, pop이 동작하니 항상 0~1사이값이 나올까?
            cout << q.size() << endl;

            // Nope 엄청큰수가 나올때가 있다.
            // -> mutex lock과 event과 묶여있지 않기에 순서에 의해 push가 더 많이 들어갈 수 있음.
        }
    }
}

int main()
{
    handle = ::CreateEvent(NULL, FALSE, FALSE, NULL);

    std::thread t1(Producer);
    std::thread t2(Consumer);

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

    ::CloseHandle(handle);
}

Condition Variable

Event의 변종!

mutex m;
queue<int32> q;

// 여기서 부터 시작
// condition_varible은 표준이기에 그냥 사용이 가능!
// 참고, CV는 User-Level Object이다. (커널 오브젝트가 아니다)
condition_variable cv;


void Producer()
{
    while(ture)
    {
        // 1) Lock을 잡고
        // 2) 공유 변수 값을 수정
        // 3) Lock을 풀고
        // 4) CV를 통해 다른 쓰레드에게 통지(이벤트를 안씀)

        {
            unique_lock<mutex> lock(m);
            q.push(100);
        }
        
        // 여기선 lock이 풀리게 되고
        
        cv.notify_one(); // wait중인 쓰레드가 있으면 딱 하나만 깨운다.
    }
}

void Consumer()
{
    while(true)
    {
        unique_lock<mutex> lock(m);
        cv.wait(lock, [](){return q.empty() == false;}/*깨어나는 조건*/);
        // 1) Lock을 잡고
        // 2) 조건 확인
        // 조건 만족 -> 빠져 나와서 이어서 코드를 진행
        // 조건 불만족 -> lock을 풀어주고 대기 상태로 빠진다
        // 주의할 점은 일반 lock은 사용이 불가능(unique_lock만 사용가능, cv에서 lock을 풀수 없다)
        // 3) notify_one이 도착하면 1)부터 다시 시작

        // 그런데 notify_one을 받을꺼면, 굳이 깨어나는 조건을 정한 이유가 있나?
        // -> Spurious Wakeup(가짜 기상) : notify_one을 받았지만, 실제론 깨어날 이유가 없었을 수 있음 예를들어 q.empty인 경우

        // 결론적으로 event를 기다리며, 자기가 깨어날 이유가 없을 경우 스스로 스레드를 잠들게 한다.(좋구만)

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

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

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

About Taehyung Kim

안녕하세요? 8년차 현업 C++ 개발자 김태형이라고 합니다. 😁 C/C++을 사랑하며 다양한 사람과의 협업을 즐깁니다. ☕ 꾸준한 자기개발을 미덕이라 생각하며 노력중이며, 제가 얻은 지식을 홈페이지에 정리 중입니다. 좀 더 상세한 제 이력서 혹은 Private 프로젝트 접근 권한을 원하신다면 메일주세요. 😎

Star
Useful Links