↓Skip to main content
  1. Posts/

Game Server Development #9 : Condition Variable

Korean Post Programming C++ Server Thread Condition Variable
Table of Contents
Game Server Development - This article is part of a series.
Part 9: This Article

Material #

[C++과 언리얼로 만드는 MMORPG 게임 개발 시리즈] Part4: 게임 서버

Problem of Event #

Game Server Development #8 : Event
Korean Post Programming C++ Server Thread Event

위 글에서 Windows 플렛폼에서 어떻게 Event 가 동작하는지에 대해서 간단히 알아봤었다.

하지만, 이벤트에는 몇 가지 문제점이 있다.

Non Standard #

이벤트는 Windows 플렛폼에서만 사용하는 비표준 내용이다.

Linux 같은 다른 플렛폼에서는 Semaphore 같은 것을 사용해야 한다.

이러한 표준적이지 않은 기능을 사용하면, 플렛폼에 종속적인 코드가 되어버린다.

Overkilling by using Kernel Objects #

이벤트는 Kernel 에서 동작한다.

이벤트를 사용하는 것은 잦은 Context Switch 를 발생시킨다.

Kernel Object 를 조작하는 명령을 수행하기 위해서는, User Mode 에서 Kernel Mode 로 전환해야하기 때문이다.

CV 의 경우 User Mode 에서 동작하기 때문에, Context Switch 가 발생할 가능성이 적다.

이러한 이벤트의 문제 상황을 해결하기 위해 C++11 부터 표준화된 Condition Variable 을 사용할 수 있다.

Condition Variable #

Condition Variable(이하 CV) 은 Event 와 비슷한 기능을 제공한다.

CV 는 Wait 을 하다가 Notify 가 울리면, 신호를 수신한 CV 는 Wait 을 벗어나기 위한 Condition (조건) 을 확인한다.

조건은 2가지 이다.

  1. Lock 을 획득했는가?
  2. Condition 이 만족되었는가? (Predicate 를 사용하는 경우에만 체크한다)

If Condition is Satisfied #

Lock 을 획득했고, Condition 이 만족되었다면 대기 상태를 벗어나서 다음 줄로 진행한다.

If Condition is NOT Satisfied #

Lock 을 획득하지 못한 경우, 다시 대기 상태로 돌아간다.

Lock 을 획득했으나, Condtion 이 만족되지 않았다면, Lock 을 풀고 대기 상태로 돌아간다.

CV 는 Unique Lock 을 사용한다.

Why Using Unique Lock? #

Game Server Development #3 : Lock
Korean Post Programming C++ Server Thread Lock

위 글에서 알아봤듯, Unique Lock 을 사용하면, 명시적인 lock 함수 호출 이전까지, 락을 획득하지 않은 상태로 대기할 수 있다.

또한, Scope 가 끝나기 전이라도 unlock 함수를 호출해 락을 해제할 수 있다.

조건을 만족하지 못할 경우 락을 풀어주고 다시 대기해야 하기 때문에, Unique Lock 을 사용한다고 볼 수 있다.

Spruious Wakeup #

이전 예제를 다시 생각해보자.

Consumer 가 이벤트를 받아 Wait 에서 벗어났지만, Producer 가 찰나의 순간에 먼저 다시 락을 잡고, 데이터를 넣었을 수도 있다.

만약 이러한 상태가 계속 지속된다면, Consumer 는 락을 획득하지 못해, Queue 에 접근할 수 없게 된다.

또한, Queue 에 데이터가 지속적으로 누적되어 메모리가 부족해질 수도 있다.

이러한 문제의 이유는 Event 와 Lock 이 개별적으로 동작하기 때문이다.

조건이 만족되지 않은 상태에서 대기중인 스레드가 일어나 버리는 것을 Spuirous Wakeup (가짜 기상) 이라고 한다.

Event 나 Notify 등을 받거나 운영체제의 스케쥴러가 깨워서 다음 작업을 진행하려 스레드가 일어났지만, 원하던 조건이 만족되지 않은 상태를 의미한다.

이러한 가짜 기상은 2 가지 정도의 문제로 바라볼 수 있다.

Incorrect State Progression #

실제로 처리해야 할 데이터나 상태가 준비되지 않았음에도 불구하고 작업을 진행하려 시도할 수 있다.

이러한 동작은 데이터 손상이나 논리적 오류를 초래할 수도 있다.

Wasting System Resources #

스레드가 필요 없이 깨어나 작업을 시도하면, 시스템 리소스가 낭비될 수 있다.

그러므로 최대한 Spuirous Wakeup 을 방지할 수 있는 로직을 작성해야 한다.

대부분은 Predicate 와 같은 조건을 통과하는지 체크하는 것으로 해결할 수 있다.

Condition Variable Functions #

CV 를 사용하기 위해서는 mutex 헤더를 추가해야 한다.

만약 좀 더 일반화된 상황에서 직접 Lock 을 구현해 사용하는 경우라면 std::condition_variable_any 를 사용하기 위해 condition_variable 헤더를 추가해 사용하면 된다.

CV 가 표준으로 제공하는 함수들은 다음과 같다.

Waiting #

대기를 위해 사용되는 함수 시리즈이다.

wait #

wait 은 그냥 대기하는 버전과 Predicate 를 사용하는 버전이 있다.


void wait( std::unique_lock<std::mutex>& lock );

template< class Predicate >
void wait( std::unique_lock<std::mutex>& lock, Predicate pred );

wait_for #

CV 가 깨어나거나, 지정된 시간 (Timeout Duration) 이 지나면 현재 스레드를 대기시킨다.

wait_until #

CV 가 깨어나거나, 지정된 시점 (Time Point) 에 도달할 때까지 현재 스레드를 대기시킨다.

Notifing #

대기 상태를 깨우는(Wake Up) 방법에 대한 함수 시리즈이다.

notify_one #

CV 가 대기 중인 스레드 중 하나를 깨운다.

notify_all #

CV 가 대기 중인 모든 스레드를 깨운다.

Condition Variable Example #

CV 를 사용하여 위에서 작성했던 Event 의 예제 코드를 다시 작성해보자.


#include <iostream>
#include <thread>
#include <mutex>
#include <queue>

std::mutex m;
std::queue<int> q;
std::condition_variable cv;

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

        cv.notify_one();
    }
}

void Consumer()
{
    while(true)
    {
        std::unique_lock<std::mutex> lock(m);

        //Condition Variable 이 조건을 만족할 때 까지 대기한다.
        //1. lock 을 획득하려고 시도한다.
        //2. q 가 비어있지 않은지 체크한다.
        //둘 다 만족해야 wait 을 벗어날 수 있다.
        //조건을 만족하지 못한다면, lock 을 풀고 다시 대기한다.
        cv.wait(lock, []() { return !q.empty(); });
        
        int data = q.front();
        q.pop();

        //Lock 을 잡고, 콘솔 출력을 하는 것은 좋은 습관은 아님.
        std::cout<<data<<std::endl;
    }
}

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

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

   return 0;
}

Conclusion #

표준으로 제공되는 Condition Variable 을 사용하면, 플렛폼에 종속적이지 않은 코드를 작성할 수 있다.

Event 와 Lock 이 개별적으로 동작하는 문제점을 해결할 수 있다.

Spuirous Wakeup 을 방지하기 위해 Predicate 조건을 정의하는 등의 추가적인 작업이 필요하다.

Reference #

Game Server Development - This article is part of a series.
Part 9: This Article

Related

Game Server Development #8 : Event
Korean Post Programming C++ Server Thread Event
Game Server Development #4 : Dead Lock
Korean Post Programming C++ Server Thread Lock
Game Server Development #2 : Race Condtion, Atomic
Korean Post Programming C++ Server Thread Atomic
Game Server Development #10 : Future, Promise, Packaged Task
Korean Post Programming C++ Server Thread Future Promise Packaged Task
Game Server Development #7 : Sleep
Korean Post Programming C++ Server Thread Lock
Game Server Development #6 : Spin Lock
Korean Post Programming C++ Server Thread Lock