15. 시간과 타이머

아이펀 엔진은 내부적으로 monotonic clock 을 사용하며, 개발자의 편의를 위해 참고용으로 wall clock 역시 제공합니다. 그리고 주기적으로 event 를 발생시킬 수 있는 timer 역시 제공합니다. 각각에 대한 설명은 다음과 같습니다.

15.1. Monotonic clock

Wall clock 은 현실 세계와 시간 단위가 똑같다는 장점이 있지만, 서버의 시간을 동기화할 때 시간 값이 튈 수 있다는 단점이 있습니다. 이때 서버 내부에서 특정 시간에 처리되어야 하는 일이 실행되지 않거나, 두 번 실행되는 일이 발생할 수 있습니다. 이 때문에 아이펀 엔진은 모든 시간 단위를 Monotonic clock 을 이용하며 게임 서버 개발자 역시 monotonic clock 을 사용할 것을 권장합니다.

Monotonic clock 은 이름에서 알 수 있듯이 시간 값이 단조 증가하는 시간입니다. 즉 서버의 시간을 동기화 해서 1초가 뒤로 돌아가더라도 안정적으로 증가하는 시간 값을 반환합니다. monotonic clock 은 최신 OS 들에서는 모두 제공하는 기능이며, 아이펀 엔진의 monotomic clock 은 system 에서 제공하는 monotonic clock 을 이용합니다. 아이펀 엔진의 Monotonic clock 는 마이크로세컨드까지의 정밀도를 제공합니다.

15.1.1. Monotonic clock 활용법

#include <funapi.h>

void test_function(MonotonicClock::Value old) {
  // 현재 시간은 다음처럼 얻습니다.
  MonotonicClock::Value t = MonotonicClock::Now();

  // 시간 비교는 일반 integer 와 같습니다.
  if (old < t) {
    // old 는 t 보다는 작네요.
  }

  // struct timespec 으로의 변경은 다음처럼 합니다.
  struc timesepc ts;
  MonotonicClock::ToTimespec(t, &ts);
}
using funapi;

void TestFunction(Int64 old)
{
  // 현재 시간은 다음처럼 얻습니다.
  Int64 t = MonotonicClock.Now;

  // 시간 비교는 일반 integer 와 같습니다.
  if (old < t) {
    // old 는 t 보다는 작네요.
  }
}

15.2. Wall clock

Monotonic clock 은 서버 시간을 보정하는 상황에서도 안정적으로 쓸 수 있다는 장점이 있지만, 사람이 읽을 수 있는 형태가 아니기 때문에 디버깅할 때 곤란합니다. 이 때문에 아이펀 엔진은 Wall clock 도 제공합니다.

아이펀 엔진에서의 Wall clock 은 게임 서버가 뜨는 순간의 현실 시계와 Monotonic clock 값의 차이를 기억했다가 이후에 Wall clock 이 필요한 경우 해당 차이를 반영한 값을 반영하는 것입니다. 이 때문에 아이펀 엔진의 Wall clock 역시 단조 증가합니다.

그 때문에 OS 시간이 보정될 때 아이펀 엔진의 Wall clock 값과 OS의 시간 값이 다를 수 있습니다. 그 때문에 아이펀 엔진에서는 Wall clock 을 참고용으로 쓰시는 것을 권장합니다.

15.2.1. Wall clock 활용법

#include <funapi.h>

void test_function(WallClock::Value old) {
  // 현재 시간은 다음처럼 얻습니다.
  WallClock::Value t = WallClock::Now();

  // 시간 비교는 일반 integer 와 같습니다.
  if (old < t) {
    // old 는 t 보다는 작네요.
  }

  // 로그로 쉽게 출력할 수 있습니다.
  LOG(INFO) << t;

  // Monotonic clock 값으로 변환할 수도 있습니다.
  MonotonicClock::Value m = WallClock::ToMonotonicClock(t);
}
using funapi;

public void TestFunction(System.DateTime old)
{
  // 현재 시간은 다음처럼 얻습니다.
  System.DateTime dt = WallClock.Now;

  // 시간 비교는 일반 DateTime 과 같습니다.
  if (old < t) {
    // old 는 t 보다는 작네요.
  }

  // Monotonic clock 값으로 변환할 수도 있습니다.
  Int64 m = WallClock.ToMonotonicClock (dt);
}

15.2.2. Timestamp 로의 전환

아래처럼 Wall clock 을 정수값의 timestamp 로 변환할 수 있습니다.

// 현재 시간에 대한 timestamp 를 얻거나
int64_t ts = WallClock::GetTimestampInSec();

// WallClock::Value 값을 timestamp 로 변환할 수 있습니다.
WallClock::Value t = WallClock::Now();
ts = WallClock::GetTimestampInSec(t);

// 반대로 timestamp 값을 WallClock::Value 로도 변환할 수 있습니다.
t = WallClock::FromTimestampInSec(ts);

// 밀리초 단위로도 가능합니다.
ts = WallClock::GetTimestampInMsec();
ts = WallClock::GetTimestampInMsec(t);
t = WallClock::FromTimestampInMsec(ts);
// 현재 시간에 대한 timestamp 를 얻거나
Int64 ts = WallClock.GetTimestampInSec ();


// System.DateTime 값을 timestamp 로 변환할 수 있습니다.
System.DateTime dateTime = WallClock.Now;
ts = WallClock.GetTimestampInSec (dateTime);

// 반대로 timestamp 값을 System.DateTime 로도 변환할 수 있습니다.
dateTime = WallClock.FromTimestampInSec (ts);

// 밀리초 단위로도 가능합니다.
ts = WallClock.GetTimestampInMsec ();
ts = WallClock.GetTimestampInMsec (dateTime);
dateTime = WallClock.FromTimestampInMsec (ts);

15.2.3. 시간의 문자열 표현

아래처럼 Wall clock 을 문자열 형태로 변환할 수 있습니다.

Note

시간에 대한 문자열 표현은 ISO-extended 형식으로 표현됩니다. 예) 2016-09-21T03:30:16.787663

// 현재 시간에 대한 문자열을 얻거나
string ts = WallClock::GetTimestring();

// WallClock::Value 값을 문자열로 변환할 수 있습니다.
WallClock::Value t = WallClock::Now();
ts = WallClock::GetTimestring(t);

// 반대로 문자열을 WallClock::Value 로도 변환할 수 있습니다.
if (not WallClock::FromTimestring(ts, &t)) {
  // ts 가 올바른 iso-extended time string 이 아니면 실패합니다.
}
// 현재 시간에 대한 문자열을 얻거나
string timestr = WallClock.GetTimestring ();

// System.DateTime 값을 문자열로 변환할 수 있습니다.
System.DateTime t = WallClock.Now;
timestr = WallClock.ToTimestring (t);

Log.Info (timestr);
// 반대로 문자열을 System.DateTime 로도 변환할 수 있습니다.
if (!WallClock.FromTimestring (timestr, out t)) {
  // timestr 가 올바른 iso-extended time string 이 아니면 실패합니다.
}

15.3. 타이머

게임 서버들은 대개 주기적으로 처리하는 일들이 있고, 이를 위해 ‘Tick’ 이라고 불리는 반복되는 timer 가 필요합니다. 아이펀 엔진은 반복되는 타이머와 일회성 타이머를 지원합니다. 아이펀 엔진 타이머는 타이머가 expire 될 때 호출될 핸들러를 등록하는 방식으로 사용합니다. 아래는 그 예제입니다.

15.3.1. 타이머 활용법

#include <funapi.h>

extern void handler1(Timer::Id tid, const WallClock::Value &at);
extern void handler2(Timer::Id tid, const WallClock::Value &at);
extern void handler3(Timer::Id tid, const WallClock::Value &at);

void test_function() {
  // 특정 시간에 expire 되게 하려면 ExpireAt 을 이용합니다.
  // 아래 예는 지금으로부터 500msec 뒤에 expire 하도록 설정합니다
  Timer::ExpireAt(WallClock::Now() + boost::posix_time::millisec(500), handler1);

  // 상대 시간 뒤에 expire 되게 하려면 ExpireAfter 를 이용합니다.
  Timer::ExpireAfter(boost::posix_time::millisec(500), handler2);

  // 반복해서 expire 되는 timer 는 interval 과 핸들러를 지정합니다.
  // 아래는 500msec 단위로 expire 되는 timer 입니다.
  Timer::ExpireRepeatedly(boost::posix_time::millisec(500), handler3);
}

Important

Timer::ExpireAt(), Timer::ExpireAfter(), Timer::ExpireRepeatedly(), Timer::Cancel() 함수들은 의도하지 않은 중복 호출을 방지하기 위하여 오브젝트 롤백을 감지하는 기능이 내장되어 있습니다. 오브젝트 서브시스템의 기능을 이용하신다면 트랜잭션 을 참고하세요.

using funapi;

public void TestFunction()
{
  // 특정 시간에 expire 되게 하려면 ExpireAt 을 이용합니다.
  // 아래 예는 지금으로부터 500msec 뒤에 expire 하도록 설정합니다
  Timer.ExpireAt (
      WallClock.Now + WallClock.FromMsec(1500), TimerHandler1);


  // delegate를 이용해 타이머를 호출 할수도 있습니다.
  Timer.Handler timer_handler2 = (
      /*UInt64 tid*/ tid,
      /*System.DateTime*/ value) => {
    Log.Info ("timer_handler_2");
  };

  // 반복해서 expire 되는 timer 는 interval 과 핸들러를 지정합니다.
  // 아래는 500msec 단위로 expire 되는 timer 입니다.
  // delegate를 이용해 호출하겠습니다.
  Timer.ExpireRepeatedly (WallClock.FromSec (1), timer_handler2);

  // 반복해서 expire 되는 timer 는 interval 과 핸들러를 지정합니다.
  // 아래는 500msec 단위로 expire 되는 timer 입니다.
  Timer.ExpireAfter (WallClock.FromMsec(500), TimerHandler3);
}

public void TimerHandler1(UInt64 tid, DateTime value)
{
  Log.Info ("TimerHandler_1");
}

public void TimerHandler3(UInt64 tid, DateTime value)
{
  Log.Info ("TimerHandler_3");
}

Important

Timer.ExpireAt(), Timer.ExpireAfter(), Timer.ExpireRepeatedly(), Timer::Cancel() 함수들은 의도하지 않은 중복 호출을 방지하기 위하여 오브젝트 롤백을 감지하는 기능이 내장되어 있습니다. 오브젝트 서브시스템의 기능을 이용하신다면 트랜잭션 을 참고하세요.