C++Builder Programming Forum
C++Builder  |  Delphi  |  FireMonkey  |  C/C++  |  Free Pascal  |  Firebird
볼랜드포럼 BorlandForum
 경고! 게시물 작성자의 사전 허락없는 메일주소 추출행위 절대 금지
C++빌더 포럼
Q & A
FAQ
팁&트릭
강좌/문서
자료실
컴포넌트/라이브러리
메신저 프로젝트
볼랜드포럼 홈
헤드라인 뉴스
IT 뉴스
공지사항
자유게시판
해피 브레이크
공동 프로젝트
구인/구직
회원 장터
건의사항
운영진 게시판
회원 메뉴
북마크
볼랜드포럼 광고 모집

C++빌더 강좌/문서
C++Builder Programming Tutorial&Docments
[216] NanoSeconds Timer 나노세컨 타이머(Thread Based)
김태선 [cppbuilder] 45606 읽음    2010-08-31 00:31
보통 우리가 사용하는 타이머는 Win32가 제공하는 것으로 ms(MilliSeconds) 단위로 작동합니다.
1 초는 1000 MilliSeconds 이니, 초당 1천번의 타이머 이벤트 발생이 가능할 것도 같습니다.
그런데 그렇게 지정을 해도 OS에서는 그렇까지 이벤트를 발생시켜 주지 않습니다.

VCL의 TTimer는 Win32의 타이머를 래핑한 것으로, 같은 성능을 가지고 있습니다.
TTimer의 Interval에 1을 지정한다고 1ms 단위로 이벤트가 발생하는 것이 아니라
대략 10~20ms 사이 값으로 이벤트가 발생합니다. 즉 1초에 60-70회쯤 발생한다는 것이죠.

이것은 20ms 이하, 넉넉잡아 40ms 이하의 빠른 속도로 타이머 이벤트를 써야 할 일이 있는 프로젝트에서는
win32의 타이머를 신뢰할 수 없다는 뜻도 됩니다.

프로젝트를 하다보면 40ms이하 또는 1ms 이하의 MicroSecond 간격으로 타이머를 써야할 일도 있습니다.
그때는 어떻게 해야 할까요?

여기서 단위에 대해 잠시 정리를 하고 넘어가죠.

1초는 1000 Milliseconds
1초는 1000000 Microseconds
1초는 1000000000 Nanoseconds

1 Millisecond는 1000 Microseconds
1 Millisecond는 1000000 Nanoseconds
1 Microsecond는 1000 Nanoseconds

win32가 제공하는 타이머 관련 함수나 기타 함수는 거의가 ms 단위입니다.
그래서 그 보다 빠른 시간 타이머를 만들려면 CPU가 제공하는 최대 시간 단위를
기준으로 쓰레드를 돌면서 시간을 체크해서 이벤트를 발생시키는 방법을 써야 합니다.
이를 위해 win32 API의 2가지 함수를 이용할 수 있습니다.
우선 QueryPerformanceFrequency(...) 는 CPU의 클락수를 가져오는 함수입니다.
CPU 속도에 맞는 클럭 값을 구할 수 있습니다.
QueryPerformanceCounter(...) 함수는 현재의 클럭 카운트를 구할 수 있습니다.

이 두 값은 CPU가 다르다면 PC 마다 다른 값이 되겠지요.
그런데 클럭수를 기준으로 값을 얻을 수 있기 때문에,
현행 2GHz 3GHz 4GHz 등등의 빠른 CPU에서의 클럭이라면 엄청난 클럭 카운트 값을 가지게 됩니다.
가령 제 PC는 지금 1.8GHz CPU인데, QueryPerformanceFrequency 함수로 조사해 보니
약 1,800,000,000 정도의 값을 가지고 있군요. CPU 속도와 값이 거의 일치하죠.
Dual CPU라고 클럭 값이 배로 나오진 않는군요.

그러면  QueryPerformanceCounter 가 현재의 CPU 클럭 카운트를 가지고 있으므로,
이 값을 저장해 놓고 다시 클럭카운트를 읽으면 저장할때와 지금과의 클럭 카운터 값 차이를 구할 수 있습니다.

클럭 카운트 차이 값 = 현재 클럭카운트 - 이전 클럭카운트;

그러면 이 클럭 카운트 차이 값을 QueryPerformanceFrequency 구해 놓았던 CPU 클럭값으로 나누면
1초 단위를 기준으로 어느 정도 시간이 경과했는지 알수 있습니다.

그래서 쓰레드 내에서 루프를 돌며 이 값 차이로 원하는 시간이 되었는지 체크할 수 있고,
원하는 시간이 되면 타이머 이벤트를 발생시켜 주면 나노세컨 까지 동작하는 타이머를 만들 수 있습니다.

사설이 긴데, 이를 클래스화한 소스를 보시면 금방 이해가 갈 것입니다.
소스는 프로젝트에 적용할 때, 이 클래스 경우는 컴포넌트 형태보다는 클래스 구현 파일 형태가
더 편리하기 때문에 헤더 파일로 만들었습니다.


#ifndef __TNSTimer_H
#define __TNSTimer_H


/*
	NanoSeconds Timer 나노세컨 타이머(쓰레드 베이스)

	컴퓨터 성능에 따라 나노초(1/1000000000초) 수준까지 타이머 실행 가능.
	WIn32의 타이머나, VCL의 TTimer에 비해 정확도가 높으며
	기존 타이머가 최소 10~40ms 정도의 인터벌을 가질수 있는 것에 비해
	이 나노세컨 타이머는 나노급 시간까지도 가능하다. 물론 컴퓨터 성능이 받쳐준다면.

	기본 동작은 1 MilliSeconds로 동작한다.

	주지:
		쓰레드 베이스로 동작하며, 쓰레드 내에서 루프를 돌며 CPU의 동작 속도를 계산해
		타이머를 동작시키는데, MilliSeconds 이하 타이머에서는
		CPU 부하를 많이 쓰는 단점이 있다. 이는 쓰레드가 Sleep 없이
		루프를 돌기 때문인데, MilliSeconds 이하 타이머 동작에서는 Sleep을 쓸수가 없다.
		Sleep의 기본적인 동작은 CPU가 본 쓰레드에 활당된 Instruction 동작을 멈추고
		다른 프로세스에 CPU를 양보하는 것인데, 이를 ContextSwitching 이라고 하고
		이 동작이 MilliSeconds 급으로 이루어지기 때문이다.
		즉 한번 ContextSwitching 이 일어나서 CPU의 제어권을 다른 프로세스로 넘겼다가
		이 쓰레드가 다시 받기까지는 최소 MilliSeconds의 시간을 소비하기 때문에
		Sleep을 써서는 MilliSeconds 이하 타이머를 만들수 없다.

		그래서 MilliSeconds 이하급 타이머에서는 항상 bUseSleep = false 상태에서
		동작해야 하며, CPU 부하를 많이 쓰기 때문에, 이를 고려해 적정한 시간 동안 쓰도록 해야 한다.
		CPU 부하를 많이 쓴다고 했는데, OS는 하나의 프로세스에 과도한 CPU 부하가 걸리지 않게
		적절하게 ContextSwitching을 하기 때문에, 지나치게 높은 수준으로
		다른 쓰레드의 동작에 큰 영향을 줄 정도로는 절대 높아지지 않는다.


	Millisecond = 1/1000초
	Microsecond = 1/1000000초
	Nanoseconds = 1/1000000000초
	1 Millisecond = 1000 Microseconds
	1 Millisecond = 1000000 Nanoseconds

	Written by 김태성.
*/
class TNSTimer : public TThread
{
private:
	int		NanoSeconds;
	bool	FEnabled;
	LARGE_INTEGER  NSTime, NSFreq;

private:
	double __fastcall GetMilliSeconds()
	{
		return NanoSeconds / 1000000.0;
	}
	void __fastcall SetMilliSeconds(double ms)
	{
		NanoSeconds = ms * 1000000.0;
	}

public:
	typedef void (__closure *TNSTimerMethod)();

	TNSTimerMethod	NSTimerMethod;
	bool	bUseSleep;

public:
	__fastcall TNSTimer() : TThread(false)
	{
		NSTimerMethod = NULL;
		FEnabled = false;
		NanoSeconds = 1000000;		// default는 1 MilliSeconds
		NSTime.QuadPart = 0;
		NSFreq.QuadPart = 0;
		bUseSleep = false;

		FreeOnTerminate = true;

		SetStdTime();
	}
	void __fastcall Execute()
	{
		while(!Terminated)
		{
			if (NanoSeconds == 0 || !FEnabled)
			{
				Sleep(1);
				continue;
			}
			LARGE_INTEGER  cur_ns;
			QueryPerformanceCounter(&cur_ns);
			LONGLONG  between = cur_ns.QuadPart - NSTime.QuadPart;
			double  second = (double)between / NSFreq.QuadPart;   	// 이렇게 하면 초로 나온다.
			if (second * 1000000 * 1000 < NanoSeconds)
			{
				if (bUseSleep)
					Sleep(0);
				continue;
			}
			NSTime = cur_ns;
			if (NSTimerMethod)
				NSTimerMethod();
		}
	}
	void	SetStdTime()
	{
		QueryPerformanceCounter(&NSTime);
		QueryPerformanceFrequency(&NSFreq);
	}

	// 나노세컨으로 인터벌 지정, 1은 백만분의 1밀리세컨. 100000은 0.1 밀리세컨.
	__property int	NanoSecondsInterval = { read=NanoSeconds, write=NanoSeconds };

	// -밀리세컨으로 인터벌 지정: 0.1 MiliSeconds, 0.01 MiliSeconds 식으로 지정 가능.
	__property double MilliSecondsInterval = { read=GetMilliSeconds, write=SetMilliSeconds };

	// 타이머 동작 on/off 제어
	__property bool Enabled = { read=FEnabled, write=FEnabled };
};


#endif


아래는 사용 예제입니다.
첨부한 프로그램에 들어간 코드입니다.

//---------------------------------------------------------------------------
TNSTimer	*NSTimer;
int			Count = 0;
//---------------------------------------------------------------------------
void __fastcall TForm1::Timer1Timer(TObject *Sender)
{
	Caption = Count;
	Label1->Caption = Now().TimeString();
	Image1->Left += V;
}
//---------------------------------------------------------------------------

void	TForm1::NSTimerMethod()
{
	Count++;
}

void __fastcall TForm1::FormCreate(TObject *Sender)
{
	NSTimer = new TNSTimer;
	NSTimer->MilliSecondsInterval = 1000;		// 1초에 한번 호출
	NSTimer->MilliSecondsInterval = 0.1; 		// 0.0001초 즉 0.1MilliSeconds 마다 한번 호출
	NSTimer->MilliSecondsInterval = 0.01; 		// 0.00001초 즉 0.01MilliSeconds 마다 한번 호출
	NSTimer->MilliSecondsInterval = 0.0001;		// 0.00001초 즉 100 NanoSeconds 마다 한번 호출. 이 정도이상은 컴퓨터 성능에 따라 오차가 남.
	NSTimer->MilliSecondsInterval = 0.001; 		// 0.00001초 즉 0.001MilliSeconds, 1000 NanoSeconds 마다 한번 호출
	NSTimer->MilliSecondsInterval = 10;			// 0.01초 즉 10MilliSeconds 마다 한번 호출
	NSTimer->MilliSecondsInterval = 1;			// 0.001초 즉 1MilliSeconds 마다 한번 호출
	NSTimer->NSTimerMethod = NSTimerMethod;
	NSTimer->Enabled = true;
}


예제라고 해 봐야 그냥 나노세컨 타이머 생성하고 구동하는 것 말고는
코드라고 할만한게 그다지 없습니다.

첨부된 파일에는 이 코드를 쓴 예제 실행화일과 TNSTimer 클래스 소스 파일이 있습니다.
예제 파일에는 CPU부하를 많이 쓰면서도 1ms 으로 동작하는 타이머의 예제입니다.
기존의 Win32 타이머에서는 꿈도 꿀수 없었던(?) 타이머 이벤트가 자연스레 발생합니다.
시간을 줄여 나노급까지 내려도 잘 동작합니다. 물론 시간을 자꾸 내릴수록 오차는 커지고
타이머 이벤트에서 처리하는 내용이 많아 시간을 지체하면 다음번
이벤트는 이벤트 핸들러의 동작이 끝나자 마자 다시 발생하기도 합니다.
이 이유는 위 소스 주석에 달아 놨습니다. 정확하게 말해 마이크로세컨이나 그 이하 시간 단위로
정말로 정확하게 동작하는 타이머는 ContextSwitching하는 CPU 특성상 구현이 불가능하고
거의 유사하게 동작하는 타이머만 만들 수 있을 뿐입니다.
첨부 파일은 이해를 돕기 위한 데모용이라, 아무 그림이나 올려서 테스트 하느라고 선택했는데
움직이는 그림은 자체 모자이크 처리를 하는게 좋을 것 같아서... ㅡㅡ;

사실 이 나노세컨 클래스는 잘 쓰면 정밀도 면에서 좋은 효과를 볼수 있지만
매우 정확한 시간 계량을 통한 시점에 정확한 이벤트 발생을 확보하고 싶으면
좀 더 세밀한 처리가 필요합니다. 물론 쓰레드 우선순위도 최대로 올려야 할 것이고요.
또한 단순히 이전 시간과 현재 시간의 차만 가지고 계산할 것이 아니고
절대 시간 계산이 필요할 것입니다.
또한 거의 ms 단위를 구성된 OS의 특성 때문에 시간 딜레이 문제로
클래스 주석에 달아 놨듯이 컨텍스트스위칭 등에도 신경을 써야 합니다.


일단 이 정도로 하고 보다 보완해서 쓰는 것은
쓰시는 분에게 맡겨야 겠군요.


그럼.
김도완 [purplecofe2]   2010-08-31 16:51 X
듀얼 코어 이상의 시스템에서는 RTDSC나 QueryPerformance* 을 이용할 때 예기치 않은 문제를 피하기 위해서는 SetThreadAffinityMask를 사용해서 코어 1개를 사용하도록 지정하는게 좋습니다.

http://www.geisswerks.com/ryan/FAQS/timing.html

그리고 QueryPerfirmance*은 비스타 이상에서는 HPET의 값을 읽어들일 수 있습니다. 그래서 더 정확하고 보다 빠른 호출도 가능해집니다. 윈도우즈 XP에서는 HPET를 인식은 하지만 드라이버가 지원되지 않습니다. 그리고 바이오스 스펙에서 제대로 HPET를 지원하려면 ACPI 3.0 표준 이상을 따르면 됩니다.
김태선 [cppbuilder]   2010-08-31 17:09 X
김도완님. 좋은 정보 고맙습니다.
Mins [heroms01]   2010-12-10 11:28 X
아놕... 내공이 부족해서 아직 이해를 못하네요 제길..ㅠㅠ

+ -

관련 글 리스트
216 NanoSeconds Timer 나노세컨 타이머(Thread Based) 김태선 45606 2010/08/31
Google
Copyright © 1999-2015, borlandforum.com. All right reserved.