Scenario 2: Call Functions at Timed Intervals

[Previous] [Next]

Sometimes applications need to perform certain tasks at certain times. Windows offers a waitable timer kernel object that makes it easy to get a time-based notification. Many programmers create a waitable timer object for each time-based task that the application will perform, but this is unnecessary and wastes system resources. Instead, you can create a single waitable timer, set it to the next due time, and then reset the timer for the next time, and so on. However, the code to accomplish this is tricky to write. Fortunately, you can let the new thread pool functions manage this for you.

To schedule a work item to be executed at a certain time, you first create a timer queue by calling this function:

 HANDLE CreateTimerQueue(); 

A timer queue organizes a set of timers. For example, imagine a single executable file that hosts several services. Each service might require timers to fire to help it maintain its state, such as when a client is no longer responding, when to gather and update some statistical information, and so on. It is inefficient to have a waitable timer and dedicated thread for each service. Instead, each service can have its own timer queue (a lightweight resource) and share the timer component's thread and waitable timer object. When a service terminates, it can simply delete its timer queue, which deletes all the timers created within it.

Once you have a timer queue, you can create timers in it as follows:

 BOOL CreateTimerQueueTimer( PHANDLE phNewTimer, HANDLE hTimerQueue, WAITORTIMERCALLBACK pfnCallback, PVOID pvContext, DWORD dwDueTime, DWORD dwPeriod, ULONG dwFlags); 

For the second parameter, you pass the handle of the timer queue that you want to create this timer in. If you are creating just a few timers, you can simply pass NULL for the hTimerQueue parameter and avoid the call to CreateTimerQueue altogether. Passing NULL tells the function to use a default timer queue and simplifies your coding. The pfnCallback and pvContext parameters indicate what function should be called and what should be passed to that function when the time comes due. The dwDueTime parameter indicates how many milliseconds should pass before the function is called the first time. (The value 0 causes the function to be called as soon as possible, making CreateTimerQueueTimer similar to QueueUserWorkItem.) The dwPeriod parameter indicates how many milliseconds should pass before the function is called in the future. Passing 0 for dwPeriod makes this a one-shot timer, causing the work item to be queued only once. The handle of the new timer is returned via the function's phNewTimer parameter.

The worker callback function must have the following prototype:

 VOID WINAPI WaitOrTimerCallback( PVOID pvContext, BOOL fTimerOrWaitFired); 

When this function is called, the fTimerOrWaitFired parameter is always TRUE, indicating that the timer has fired.

Now let's talk about CreateTimerQueueTimer's dwFlags parameter. This parameter tells the function how to queue the work item when the time comes due. You can use WT_EXECUTEDEFAULT if you want a non-I/O component thread to process the work item, WT_EXECUTEINIOTHREAD if you want to issue an asynchronous I/O request at a certain time, or WT_ EXECUTEINPERSISTENTTHREAD if you want a thread that never dies to process the work item. You can use WT_EXECUTELONGFUNCTION if you think that your work item will require a long time to execute.

You can also use another flag, WT_EXECUTEINTIMERTHREAD, which requires a bit more explaining. In Table 11-1, you can see that the thread pool has a timer component. This component creates the single waitable timer kernel object and manages its due time. The component always consists of a single thread. When you call CreateTimerQueueTimer, you cause the timer component's thread to wake up, add your timer to a queue of timers, and reset the waitable timer kernel object. The timer component's thread then goes into an alertable sleep, waiting for the waitable timer to queue an APC to it. After the waitable timer queues the APC, the thread wakes up, updates the timer queue, resets the waitable timer, and then decides what to do with the work item that should now be executed.

Next, the thread checks for the following flags: WT_EXECUTEDEFAULT, WT_EXECUTEINIOTHREAD, WT_EXECUTEINPERSISTENTTHREAD, WT_EXECUTELONGFUNCTION, and WT_EXECUTEINTIMERTHREAD. By now, it should be obvious what the WT_EXECUTEINTIMERTHREAD flag does: it causes the timer component's thread to execute the work item. While this makes execution of the work item more efficient, it is very dangerous! If the work item function blocks for a long time, the timer component's thread can't do anything else. The waitable timer might still be queuing APC entries to the thread, but these work items won't be handled until the currently executing function returns. If you plan to execute code using the timer thread, the code should execute quickly and should not block.

The WT_EXECUTEINIOTHREAD, WT_EXECUTEINPERSISTENTTHREAD, and WT_EXECUTEINTIMERTHREAD flags are mutually exclusive. If you don't pass any of these flags (or use the WT_EXECUTEDEFAULT flag), the work item is queued to the non-I/O component's threads. Also, the WT_EXECUTELONGFUNCTION flag is ignored if the WT_EXECUTEINTIMERTHREAD flag is specified.

When you no longer want a timer to fire, you must delete it by calling the function below.

 BOOL DeleteTimerQueueTimer( HANDLE hTimerQueue, HANDLE hTimer, HANDLE hCompletionEvent); 

You must call this function even for one-shot timers that have fired. The hTimerQueue parameter indicates which queue the timer is in. The hTimer parameter identifies the timer to delete; the handle was returned by an earlier call to CreateTimerQueueTimer.

The last parameter, hCompletionEvent, tells you when there are no outstanding work items queued because of this timer. If you pass INVALID_HANDLE_VALUE for this parameter, DeleteTimerQueueTimer does not return until all queued work items for this timer have completely executed. Think about what this means: if you do a blocking delete of a timer during its own work item processing, you create a deadlock situation, right? You are waiting for the work item to finish processing, but you are halting its processing while waiting for it to finish! A thread can do a blocking delete of a timer only if it isn't the thread processing the timer's work item.

Also, if you are using the timer component's thread, you should not attempt a blocking delete of any timer or a deadlock will occur. Attempting to delete a timer queues an APC notification to the timer component's thread. If this thread is waiting for a timer to be deleted, it can't also be deleting the timer, so a deadlock occurs.

Instead of passing INVALID_HANDLE_VALUE for the hCompletionEvent parameter, you can pass NULL. This tells the function that you want the timer deleted as soon as possible. In this case, DeleteTimerQueueTimer returns immediately, but you will not know when all of this timer's queued work items have completed processing. Finally, you can pass the handle of an event kernel object as the hCompletionEvent parameter. When you do this, DeleteTimerQueueTimer returns immediately and the timer component's thread sets the event after all of the timer's queued work items have completed processing. Make sure that before you call DeleteTimerQueueTimer, the event is not signaled, or your code will think that the queued work items have executed before they really have.

Once you create a timer, you can alter its due time or period by calling this function:

 BOOL ChangeTimerQueueTimer( HANDLE hTimerQueue, HANDLE hTimer, ULONG dwDueTime, ULONG dwPeriod); 

Here, you pass the handle of a timer queue and the handle of an existing timer that you want to modify. You can change the timer's dwDueTime and dwPeriod. Note that attempting to change a one-shot timer that has already fired has no effect. Also note that you can freely call this function without having to worry about deadlocks.

When you no longer need a set of timers, you can delete the timer queue by calling this function:

 BOOL DeleteTimerQueueEx( HANDLE hTimerQueue, HANDLE hCompletionEvent); 

This function takes the handle of an existing timer queue and deletes all of the timers in it so that you don't have to call DeleteTimerQueueTimer explicitly for every one. The hCompletionEvent parameter has the same semantics here as it does for the DeleteTimerQueueTimer function. This means that the same deadlock possibilities exist, so be careful.

Before we move on to another scenario, let me point out a couple of additional items. First, the timer component of the thread pool creates the waitable timer so that it queues APC entries rather than signaling the object. This means that the operating system queues APC entries continuously, and timer events are never lost. So setting a periodic timer guarantees that your work item is queued at every interval. If you create a periodic timer that fires every 10 seconds, your callback function is called every 10 seconds. Be aware that this will happen using multiple threads; you might have to synchronize portions of your work item function.

If you don't like this behavior and would prefer that your work items be queued 10 seconds after each one executes, you should create one-shot timers at the end of your work item function. Or you can create a single timer with a high timeout value and call ChangeTimerQueueTimer at the end of the work item function.

The TimedMsgBox Sample Application

The TimedMsgBox application ("11 TimedMsgBox.exe"), listed in Figure 11-1, shows how to use the thread pool's timer functions to implement a message box that automatically closes if the user doesn't respond within a certain amount of time. The source code and resource files for the application are in the 11-TimedMsgBox directory on the book's companion CD-ROM.

When you start the program, it sets a global variable, g_nSecLeft, to 10. This indicates the number of seconds that the user has to respond to the message box. Then CreateTimerQueueTimer is called, instructing the thread pool to call the MsgBoxTimeout function every second. Once everything has been initialized, MessageBox is called and presents the following message box to the user.

While waiting for the user to respond, the MsgBoxTimeout function is called by a thread pool thread. This function finds the window handle for the message box, decrements the global g_nSecLeft variable, and updates the string in the message box. After MsgBoxTimeout has been called the first time, the message box looks like this.

When MsgBoxTimeout is called for the tenth time, the g_nSecLeft variable becomes 0 and MsgBoxTimeout calls EndDialog to destroy the message box. The primary thread's call to MessageBox returns, DeleteTimerQueueTimer is called to tell the thread pool to stop calling the MsgBoxTimeout function, and another message box appears, telling the user that he or she didn't respond to the first message box in the allotted period.

If the user does respond before the time runs out, the following message box appears.

Figure 11-1. The TimedMsgBox sample application

TimedMsgBox.cpp

 /****************************************************************************** Module: TimedMsgBox.cpp Notices: Copyright (c) 2000 Jeffrey Richter ******************************************************************************/ #include "..\CmnHdr.h" /* See Appendix A. */ #include <tchar.h> ////////////////////////////////////////////////////////////////////////////// // The caption of our message box TCHAR g_szCaption[] = TEXT("Timed Message Box"); // How many seconds we'll display the message box int g_nSecLeft = 0; // This is STATIC window control ID for a message box #define ID_MSGBOX_STATIC_TEXT 0x0000ffff ////////////////////////////////////////////////////////////////////////////// VOID WINAPI MsgBoxTimeout(PVOID pvContext, BOOLEAN fTimeout) { // NOTE: Due to a thread race condition, it is possible (but very unlikely) // that the message box will not be created when we get here. HWND hwnd = FindWindow(NULL, g_szCaption); if (hwnd != NULL) { // The window does exist; update the time remaining. TCHAR sz[100]; wsprintf(sz, TEXT("You have %d seconds to respond"), g_nSecLeft--); SetDlgItemText(hwnd, ID_MSGBOX_STATIC_TEXT, sz); if (g_nSecLeft == 0) { // The time is up; force the message box to exit. EndDialog(hwnd, IDOK); } } else { // The window does not exist yet; do nothing this time. // We'll try again in another second. } } ////////////////////////////////////////////////////////////////////////////// int WINAPI _tWinMain(HINSTANCE hinstExe, HINSTANCE, PTSTR pszCmdLine, int) { chWindows9xNotAllowed(); // How many seconds we'll give the user to respond g_nSecLeft = 10; // Create a multishot 1-second timer that begins firing after 1 second. HANDLE hTimerQTimer; CreateTimerQueueTimer(&hTimerQTimer, NULL, MsgBoxTimeout, NULL, 1000, 1000, 0); // Display the message box. MessageBox(NULL, TEXT("You have 10 seconds to respond"), g_szCaption, MB_OK); // Cancel the timer & delete the timer queue DeleteTimerQueueTimer(NULL, hTimerQTimer, NULL); // Let us know if the user responded or if we timed out. MessageBox(NULL, (g_nSecLeft == 0) ? TEXT("Timeout") : TEXT("User responded"), TEXT("Result"), MB_OK); return(0); } //////////////////////////////// End of File ///////////////////////////////// 

TimedMsgBox.rc

 //Microsoft Developer Studio generated resource script. // #include "resource.h" #define APSTUDIO_READONLY_SYMBOLS ///////////////////////////////////////////////////////////////////////////// // // Generated from the TEXTINCLUDE 2 resource. // #include "afxres.h" ///////////////////////////////////////////////////////////////////////////// #undef APSTUDIO_READONLY_SYMBOLS ///////////////////////////////////////////////////////////////////////////// // English (U.S.) resources #if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) #ifdef _WIN32 LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US #pragma code_page(1252) #endif //_WIN32 ///////////////////////////////////////////////////////////////////////////// // // Icon // // Icon with lowest ID value placed first to ensure application icon // remains consistent on all systems. IDI_TIMEDMSGBOX ICON DISCARDABLE "TimedMsgBox.ico" #ifdef APSTUDIO_INVOKED ///////////////////////////////////////////////////////////////////////////// // // TEXTINCLUDE // 1 TEXTINCLUDE DISCARDABLE BEGIN "resource.h\0" END 2 TEXTINCLUDE DISCARDABLE BEGIN "#include ""afxres.h""\r\n" "\0" END 3 TEXTINCLUDE DISCARDABLE BEGIN "\r\n" "\0" END #endif // APSTUDIO_INVOKED #endif // English (U.S.) resources ///////////////////////////////////////////////////////////////////////////// #ifndef APSTUDIO_INVOKED ///////////////////////////////////////////////////////////////////////////// // // Generated from the TEXTINCLUDE 3 resource. // ///////////////////////////////////////////////////////////////////////////// #endif // not APSTUDIO_INVOKED 



Programming Applications for Microsoft Windows
Programming Applications for Microsoft Windows (Microsoft Programming Series)
ISBN: 1572319968
EAN: 2147483647
Year: 1999
Pages: 193

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net