|
Introduction
Standard
Win32 timers
Multimedia
timers
Waitable
timers
Queue
timers
Make your
own timer
Conclusion
The goal of this article is to show the practical use of different kinds of
timers. First, we will see how to use the "standard" Win32 timers, than we will
switch to multimedia timers, mention waitable timers, queue timers and finally
"roll our own" timer class. I will try to make some general comparison between
these solutions. So called high-resolution timer based on functions
QueryPerformanceFrequency
and QueryPerformanceCounter
will not be taken into account, because this timer can be used only to measure
time intervals, and not to "fire events" in regular time intervals. In the first
version of this article, I decided to skip waitable timers and queue timers, but
after the critics from the readers, I am including them now.
According to MSDN, A timer is an internal routine that repeatedly measures
a specified interval, in milliseconds. It means that if we create a timer
and specify a time interval of uElapse
milliseconds, it will do
"something" every uElapse
milliseconds, until we destroy it. It is
up to us to specify what is "something".
Timers are a powerful feature offered to programmers. However, they often use much of the CPU time, and sometimes it is better to avoid them. Some programmers (especially Visual Basic programmers) tend to use timers for different kinds of polling (e.g. check every 200 milliseconds if the user entered some value into the edit box), which is almost never a good idea. Good candidates for using timers are applications which do not depend that much on users' actions, but rather on time flow.
When the term timer is used, it is almost always referred to this kind of timers. I use the term Win32 timer in this article, simply to make distinction between them and other timers. In some texts, these timers are called user timers because they are not kernel objects, unlike waitable and queue timers.
How does this kind of timers work? First, we create a timer, specify its
elapse time, and (optionally) attach it to a window. After the timer is created,
it sends WM_TIMER
messages to the window message queue, or if no
window was specified, to the application queue. We can process this message to
call the code that we want to be executed in regular time intervals. The timer
will send WM_TIMER
messages until it is destroyed.
To create a timer, we will use a Win32 function:
UINT_PTR SetTimer(HWND hWnd, UINT_PTR nIDEvent, UINT uElapse, TIMERPROC
lpTimerFunc);
or its MFC equivalent:
UINT CWnd::SetTimer(UINT nIDEvent, UINT nElapse, <SPAN
class=cpp-keyword>void (CALLBACK EXPORT* lpfnTimer)(HWND, UINT, UINT,
DWORD));
|
the handle of the window to which the timer is associated; may be
| |
|
a nonzero timer identifier. | |
|
timer's time-out interval in milliseconds. |
|
|
an application-defined callback function that processes
|
The timer identifier. If hWnd
is non-NULL
, than it
is equal to nIDEvent
. In case of error it is zero.
Now, at some point we may want to stop this "ticking". We can do this by destroying the timer:
BOOL KillTimer(HWND hWnd, UINT_PTR uIDEvent);
or its MFC equivalent:
BOOL CWnd::KillTimer(int
nIDEvent);
hWnd
- the same value as in the call to
SetTimer
.uIDEvent
- the timer identifier.
If the function succeeds, TRUE
; if it fails, FALSE
A typical use of Win32 timers from a CWnd
- derived class, looks
like this:
void CTimersDlg::OnButtonBegin() { . . . // create the timer SetTimer (m_nTimerID, uElapse, NULL); } void CTimersDlg::OnButtonStop() { // destroy the timer KillTimer(m_nTimerID); } void CTimersDlg::OnTimer(UINT nIDEvent) // called every uElapse milliseconds { // do something . . . CDialog::OnTimer(nIDEvent); }
So far, so good. Why would we ever want to work with any other kind of
timers? Well, it depends on the desired accuracy of the timer. If we want to
check our Inbox for new mail every half an hour, Win32 timers are all we want.
But for more accurate time measurement (elapsed time less than 1 sec), these
timers are hardly the solution. The main reason is that timer posts
WM_TIMER
messages to a message queue, and we can never be sure when
this message will be processed. Now, you might think (as I used to) that setting
lpTimerFunc
may be a solution to this problem, but that is not
true. If you specify lpTimerFunc
, the default window procedure
calls it when it processes WM_TIMER
, not before. So, we will still
wait for WM_TIMER
to be processed.
The multimedia timer is quite a different creature. It does not post any messages to message queues, but instead calls the specified callback function directly (or, alternatively, it can set or pulse the specific event, but that option will not be covered in this article). Therefore, it is more accurate than the standard Win32 timer, but also more dangerous. Here, we do not have a message queue to protect us if we specify short elapse time.
To use multimedia timers in your projects, you should include
Mmsystem.h
, and to link it with Winmm.lib
The first step when using multimedia timers, is setting the timer resolution. What is timer resolution? It determines the accuracy of the timer. For instance, if elapse time is 1000, and resolution is 50, multimedia timer will "tick" every 950 - 1050 milliseconds.
That sounds great. Why don't we just set the timer resolution to zero, and have an absolutely accurate timer? That's because different systems support different minimum values for the multimedia timer resolution. We can obtain this minimum value by calling:
MMRESULT timeGetDevCaps(LPTIMECAPS ptc, UINT cbtc);
ptc
- pointer to a TIMECAPS
structure. It is filled with information about the resolution of the timer
devicecbtc
- size of TIMECAPS (<SPAN
class=cpp-keyword>sizeof (TIMECAPS)
).
TIMERR_NOERROR
if successful or
TIMERR_STRUCT
if it fails.
TIMECAPS is pretty simple:
typedef struct { UINT wPeriodMin; UINT wPeriodMax; } TIMECAPS;
wPeriodMin
- Minimum supported
resolution.wPeriodMax
- Maximum supported resolution.
We need to pick our minimum resolution to be in this range. Now, when we have it, let's set the resolution. We will do it by calling the function:
MMRESULT timeBeginPeriod(UINT uPeriod);
uPeriod
- Minimum timer resolution.
TIMERR_NOERROR
if successful or TIMERR_NOCANDO
if
the resolution specified in uPeriod is out of range.
Now, that we set the resolution, let's create the timer. The multimedia timer
equivalent of SetTimer
, looks like this:
MMRESULT timeSetEvent(UINT uDelay, UINT uResolution, LPTIMECALLBACK
lpTimeProc, DWORD dwUser, UINT fuEvent);
uDelay
- Event delay, in milliseconds. Pretty much the same as
uElapse
in SetTimer.
uResolution
-
Resolution of the timer event, in milliseconds. lpTimeProc
-
Pointer to the callback function that we want to be called
periodically.dwUser
- User data passed to the callback
function.fuEvent
- Timer event type. May be either
TIME_ONESHOT
, in which case the callback function is called only
once, or TIME_PERIODIC
for periodic calling.
An identifier for the timer event if successful or NULL if it fails.
Let's take a look at the callback function. It is declared like this:
void CALLBACK TimeProc(UINT uID, UINT
uMsg, DWORD dwUser, DWORD dw1, DWORD dw2);
uID
- Timer ID, returned by
timeSetEvent
uMsg
- Reserved.
lpTimeProc
- Pointer to the callback function that we want to
be called periodically.dwUser
- User data passed to the
callback function.dw1, dw2
- Reserved.
Eventually, we will need to destroy the timer. We can accomplish this by a call to the function:
MMRESULT timeKillEvent(UINT uTimerID);
uTimerID
- Timer ID.
TIMERR_NOERROR
if successful or MMSYSERR_INVALPARAM
if the specified timer event does not exist.
Remember setting the timer resolution? Well, after we are finished with the timer, we should "reset" the timer resolution with a call to:
MMRESULT timeEndPeriod(UINT uPeriod);
uPeriod
- The same as in timeBeginPeriod
.
TIMERR_NOERROR
if successful or TIMERR_NOCANDO
if
fails.
The multimedia timer version of the example from the previous chapter:
void CTimersDlg::OnButtonBegin() { . . . // Set resolution to the minimum supported by the system TIMECAPS tc; timeGetDevCaps(&tc, sizeof(TIMECAPS)); DWORD resolution = min(max(tc.wPeriodMin, 0), tc.wPeriodMax); timeBeginPeriod(resolution); // create the timer m_idEvent = timeSetEvent( m_elTime, resolution, TimerFunction, (DWORD)this, TIME_PERIODIC); } void CTimersDlg::OnButtonStop() { // destroy the timer timeKillEvent(m_idEvent); // reset the timer timeEndPeriod (m_elTime); } void CTimersDlg::MMTimerHandler(UINT nIDEvent) // called every elTime milliseconds { // do what you want to do, but quickly . . . } void CALLBACK TimerFunction(UINT wTimerID, UINT msg, DWORD dwUser, DWORD dw1, DWORD dw2) { // This is used only to call MMTimerHandler // Typically, this function is static member of CTimersDlg CTimersDlg* obj = (CTimersDlg*) dwUser; obj->MMTimerHandler(wTimerID); }
The example shown above is written in a way to resemble the handling of standard Win32 timers. In practice, however, I wrap the functionality of multimedia timers in a separate class, and I recommend you to do the same.
These multimedia timers are just perfect, aren't they? Well, almost. If the resolution is low, or the callback function is lengthy, the system overhead may be very high, and that's why we need to be very careful with them.
Waitable timers were introduced with Windows 98 and Windows NT 4.0 (which means that we need to define _WIN32_WINNT >= 0x0400, or _WIN32_WINDOWS > 0x0400 before we include windows.h, to work with them), and their main purpose is thread synchronization. These timers are kernel objects which are signaled in the specified time, or in regular time intervals. They can specify the callback function (actually, an asynchronous procedure call, or APC) which gets called when timer gets signaled. This callback function is usually called a completion routine.
In order to enable execution of the completion routine, the thread must be in
alertable state (executing SleepEx()
, WaitForSingleObjectEx()
, WaitForMultipleObjectsEx()
, MsgWaitForMultipleObjectsEx()
, SignalObjectAndWait()
functions). In practice, this means that
our main thread will be blocked while we are using waitable timers.
To start working with a waitable timer, we must open an existing timer, or create the new one. Creating can be accomplished with a call to:
HANDLE CreateWaitableTimer(LPSECURITY_ATTRIBUTES lpTimerAttributes,
BOOL bManualReset, LPCTSTR lpTimerName);
lpTimerAttributes
- pointer to a SECURITY_ATTRIBUTES
structure that specifies a security descriptor for the waitable timer object.
Can be NULL
.bManualReset
- specifies whether the waitable timer is manual-reset
or auto-reset.lpTimerName
- the name of the new timer. Can be NULL
Another possibility is to open an existing named waitable timer.
Now, when we have a handle to the waitable timer object, we can do something useful with it. To set it, we will use the function:
BOOL SetWaitableTimer(HANDLE hTimer, const LARGE_INTEGER *pDueTime,
LONG lPeriod, PTIMERAPCROUTINE pfnCompletionRoutine, LPVOID lpArgToCompletionRoutine,
BOOL fResume);
hTimer
- a handle to the timer object.pDueTime
- specifies when the state of the timer is to be set
to signaled.lPeriod
- the period of the timer in milliseconds, like uElapse
in SetTimer()
.pfnCompletionRoutine
- the pointer to a completion routine. Can be
NULL
.fResume
- Specifies whether to restore a system in suspended power conservation
mode when the timer state is set to signaled.
Finally, here is a function that stops the waitable timer:
BOOL CancelWaitableTimer(HANDLE hTimer);
hTimer
- a handle to the timer object.The example will be a little different this time:
void CTimersDlg::OnButtonBegin() { . . . // create the timer timerHandle = CreateWaitableTimer(NULL, FALSE, NULL); // set the timer LARGE_INTEGER lElapse; lElapse.QuadPart = - ((int)elapse * 10000); BOOL succ = SetWaitableTimer(timerHandle, &lElapse, elapse, TimerProc, this, FALSE); for (int i = 0; i < 10; i++) SleepEx(INFINITE, TRUE); CancelWaitableTimer(timerHandle); CloseHandle (timerHandle); } void CTimersDlg::WaitTimerHandler() // called every elTime milliseconds { // do what you want to do, but quickly . . . } void CALLBACK (LPVOID lpArgToCompletionRoutine, DWORD dwTimerLowValue, DWORD dwTimerHighValue) { // This is used only to call WaitTimerHandler // Typically, this function is static member of CTimersDlg CTimersDlg* obj = (CTimersDlg*) lpArgToCompletionRoutine; obj->WaitTimerHandler(); }
As you can see, we don't have OnButtonStop() now. As soon as we set the timer, we must put our calling thread into alertable state, and wait. This means that we can not do anything in the main thread until we finish with the timer. Of course, nothing prevents us to launch a separate thread which won't be blocked. In fact, it would be much better idea to set a timer in a separate worker thread, so that this another thread is blocked, instead of the main thread.
What can we conclude about waitable timers? They do not spend much CPU time and they do not need message queue. The main problem is that the thread which sets the waitable timer must put itself in an alertable state, or the completion routine will never be called. This fact significantly reduces usability of waitable timers.
The last kind of Windows - supported timers that we are going to read about in this article is queue timers. They were introduced with Windows 2000.
Queue timers are lightweight kernel objects that reside in timer queues. Like most timers, they enable us to specify the callback function to be called when the specified due time arrives. In this case, the wait operation is performed by a thread in the thread pool.
Here, for the sake of simplicity, we are not going to create our timer queues. Instead, we will put our queue timers into default timer queue, provided by the OS.
First, we need to create a timer and add it to the default timer queue. For this, we'll make a call to:
BOOL CreateTimerQueueTimer(PHANDLE phNewTimer, HANDLE TimerQueue
, WAITORTIMERCALLBACK Callback, PVOID Parameter, DWORD DueTime, DWORD Period, ULONG Flags);
phNewTimer
- pointer to a handle; this is an out valueTimerQueue
- timer queue handle. For the default timer queue, NULL
Callback
- pointer to the callback functionParameter
- value passed to the callback functionDueTime
- time (milliseconds), before the timer is set to the signaled
state for the first time Period
- timer period (milliseconds). If zero, timer is signaled only onceFlags
- one or more of the next values (table taken from MSDN):WT_EXECUTEINTIMERTHREAD | The callback function is invoked by the timer thread itself. This flag should be used only for short tasks or it could affect other timer operations. |
WT_EXECUTEINIOTHREAD | The callback function is queued to an I/O worker thread. This
flag should be used if the function should be executed in a thread that waits in
an alertable state.
The callback function is queued as an APC. Be sure to address reentrancy issues if the function performs an alertable wait operation. |
WT_EXECUTEINPERSISTENTTHREAD | The callback function is queued to a thread that never
terminates. This flag should be used only for short tasks or it could affect
other timer operations.
Note that currently no worker thread is persistent, although no worker thread will terminate if there are any pending I/O requests. |
WT_EXECUTELONGFUNCTION | Specifies that the callback function can perform a long wait. This flag helps the system to decide if it should create a new thread. |
WT_EXECUTEONLYONCE | The timer will be set to the signaled state only once. |
The callback function is really pretty simple:
VOID CALLBACK WaitOrTimerCallback(PVOID lpParameter, BOOLEAN TimerOrWaitFired);
lpParameter
- pointer to user-defined data.TimerOrWaitFired
- always TRUE for timer callbacks.To cancel a queue timer, use the function:
BOOL DeleteTimerQueueTimer(HANDLE TimerQueue, HANDLE Timer, HANDLE CompletionEvent);
TimerQueue
- a handle to the (default) timer queue.Timer
- a handle to the timer.CompletionEvent
- a handle to an optional event to be signaled when the function is successful and all callback
functions have completed. Can be NULL.The example for queue timers is given below:
void CTimersDlg::OnButtonBegin() { . . . // create the timer BOOL success = ::CreateTimerQueueTimer( &m_timerHandle, NULL, TimerProc, this, 0, elTime, WT_EXECUTEINTIMERTHREAD); } void CTimersDlg::OnButtonStop() { // destroy the timer DeleteTimerQueueTimer(NULL, m_timerHandle, NULL); CloseHandle (m_timerHandle); } void CTimersDlg::QueueTimerHandler() // called every elTime milliseconds { // do what you want to do, but quickly . . . } void CALLBACK TimerProc(void* lpParametar, BOOLEAN TimerOrWaitFired) { // This is used only to call QueueTimerHandler // Typically, this function is static member of CTimersDlg CTimersDlg* obj = (CTimersDlg*) lpParametar; obj->QueueTimerHandler(); }
As you can see, queue timers are pretty easy to use. I can also add that they are very accurate, and "resource friendly".
Alas, as I noted at the beginning of this chapter, queue timers are supported only on Windows 2000 and higher. If you do not want to support older Windows versions, they are perfect.
The question you will ask when you start reading this chapter is: "Why should we develop new timers, when we have four kinds of timers provided by Windows?". Indeed, for almost everything you might want to do in specific time intervals, the timers described above will satisfy your needs. To be quite honest with you, in all the cases that I needed timers, I used standard Win32 timers, except for one project, when I used the multimedia timer to control the work of a pump. However, sometimes you may want to have more control over the timer capabilities, or to make less "OS specific" timers, and here you are.
The general idea, described in this article, is to launch a separate thread which will sleep for the specified time, then call the callback function, than sleep again, and so on until we destroy the timer. Now, I do not claim that this is the best way to make a timer, it is just the simplest that I can think of. Of course, this approach has its drawbacks. The most obvious is that handling threads can often be tricky, especially when it comes to synchronization. It is much harder to debug a multithreaded application, than a single-threaded one. Also, if we want our timer to be more accurate, we will need to mess with scheduling thread priorities.
Having said that, we are going to develop our own timer class. The class declaration might look like this:
typedef void (*ThreadTimerProc)(void* obj, UINT idEvent); class CThreadTimer { void* object; // pointer to the "parent" object (like CTimersDlg) UINT idEvent; // timer ID UINT elapse; // "Sleep time" in milliseconds ThreadTimerProc proc; // Callback function, supplied by the user BOOL isActive; // Set to FALSE after the call to KillTimer CRITICAL_SECTION lock; // thread synchronization static DWORD WINAPI ThreadFunction (LPVOID pParam); // thread entry point public: CThreadTimer(); virtual ~CThreadTimer(); UINT SetTimer (void* obj, UINT nIDEvent, UINT uElapse, ThreadTimerProc lpTimerProc); BOOL KillTimer(); };
Implementation part is shown below:
CThreadTimer::CThreadTimer():object(0),idEvent(0),elapse(0), isActive(FALSE) { InitializeCriticalSection(&lock); } CThreadTimer::~CThreadTimer() { DeleteCriticalSection(&lock); } UINT CThreadTimer::SetTimer (void* obj, UINT nIDEvent, UINT uElapse, ThreadTimerProc lpTimerProc) { object = obj; idEvent = nIDEvent; elapse = uElapse; proc = lpTimerProc; EnterCriticalSection(&lock); // is it already active? if (isActive) { LeaveCriticalSection(&lock); return 0; } // Start the thread DWORD threadId; HANDLE threadHandle = CreateThread (NULL, 0, CThreadTimer::ThreadFunction, this, 0, &threadId); SetThreadPriority(threadHandle,THREAD_PRIORITY_TIME_CRITICAL); // this is optional isActive = TRUE; LeaveCriticalSection(&lock); return nIDEvent; } BOOL CThreadTimer::KillTimer() { EnterCriticalSection(&lock); isActive = FALSE; LeaveCriticalSection(&lock); return TRUE; } DWORD WINAPI CThreadTimer::ThreadFunction (LPVOID pParam) { // Here is the heart of our little timer CThreadTimer* obj = (CThreadTimer*) pParam; BOOLEAN isActive = TRUE; do { Sleep(obj->elapse); obj->proc (obj->object, obj->idEvent); EnterCriticalSection(&obj->lock); isActive = obj->isActive; LeaveCriticalSection(&obj->lock); } while (isActive); return 0; }
Finally, let's see the example from the previous chapters, rewritten to use our thread timer class:
void CTimersDlg::OnButtonBegin() { . . . // create the timer m_timer.SetTimer (this, m_nTimerID, uElapse, TimerFunction); } void CTimersDlg::OnButtonStop() { // destroy the timer m_timer.KillTimer(); } void CTimersDlg::HandleTimer(UINT nIDEvent) // called every uElapse milliseconds { // do something . . . } void TimerFunction(void* obj, UINT idEvent) { CTimersDlg* dlg = (CTimersDlg*) obj; dlg->HandleTimer(idEvent); }
How accurate such timer can be? Pretty accurate, believe it or not. When I
tested CThreadTimer
, I found that it is somwhere in between the
standard Win32 timer and multimedia timer. However, when I changed the process
priority to Realtime (using Task manager on my Windows 2000),
CThreadTimer
seemed to work even better than the multimedia timer.
Now, I do not imply that it is wise to change your process priority for the sake
of timer accuracy, but sometimes it may be a solution.
Well, what's the moral of the whole story?
When you decide that you need a timer in your application (for some good reason, and not for polling), choosing between the different timer variants should not be that hard. Follow these simple rules:
Pretty simple, isn't it? I wish queue timers were introduced with Windows 95, and than we wouldn't need to make such choices.
For professional programming, it is often the best thing to use standard, well-tested solutions. However, hacking with timers can be a real fun, and the possibilities of experimenting seem to be unlimited. Enjoy.
Born in Kragujevac, Serbia, currently living in San Diego, CA. His experience includes development of engineering software (process simulation, CAD, FEM), although he has been mostly in server-side development during the past few years.
Click here to view Nemanja Trifunovic's online profile.
Premium Sponsor |
|
Home >>
System >>
General
Advertise on The Code Project |
Article content copyright Nemanja Trifunovic, 2001 everything else © CodeProject, 1999-2001. |
DevelopersDex • DevGuru • Programmers Heaven • Tek-Tips Forums • TopXML • VisualBuilder.com • W3Schools • XMLPitstop • ZVON • Search all Partners |