Various ways of performing an operation asynchronously after a delay


Okay, if you have a UI thread that pumps messages, then the easiest way to perform an operation after a delay is to set a timer. But let's say you don't have a UI thread that you can count on.

One method is to burn a thread:

#define ACTIONDELAY (30 * 60 * 1000) // 30 minutes, say

DWORD CALLBACK ActionAfterDelayProc(void *)
{
 Sleep(ACTIONDELAY);
 Action();
 return 0;
}

BOOL PerformActionAfterDelay()
{
 DWORD dwThreadId;
 HANDLE hThread = CreateThread(NULL, 0, ActionAfterDelayProc,
                               NULL, 0, &dwThreadId);
 BOOL fSuccess = hThread != NULL;
 if (hThread) {
  CloseHandle(hThread);
 }
 return fSuccess;
}

Less expensive is to borrow a thread from the thread pool:

BOOL PerformActionAfterDelay()
{
 return QueueUserWorkItem(ActionAfterDelayProc, NULL,
                          WT_EXECUTELONGFUNCTION);
}

But both of these methods hold a thread hostage for the duration of the delay. Better would be to consume a thread only when the action is in progress. For that, you can use a thread pool timer:

void CALLBACK ActionAfterDelayProc(void *lpParameter, BOOLEAN)
{
 HANDLE *phTimer = static_cast<HANDLE *>(lpParameter);
 Action();
 DeleteTimerQueueTimer(NULL, *phTimer, NULL);
 delete phTimer;
}

BOOL PerformActionAfterDelay()
{
 BOOL fSuccess = FALSE;
 HANDLE *phTimer = new(std::nothrow) HANDLE;
 if (phTimer != NULL) {
  if (CreateTimerQueueTimer(
     phTimer, NULL, ActionAfterDelayProc, phTimer,
     ACTIONDELAY, 0, WT_EXECUTEONLYONCE)) {
   fSuccess = TRUE;
  }
 }
 if (!fSuccess) {
  delete phTimer;
 }
 return fSuccess;
}

The timer queue timer technique is complicated by the fact that we want the timer to self-cancel, so it needs to know its handle, but we don't know the handle until after we've scheduled it, at which point it's too late to pass the handle as a parameter. In other words, we'd ideally like to create the timer, and then once we get the handle, go back in time and pass the handle as the parameter to Create­Timer­Queue­Timer. Since the Microsoft Research people haven't yet perfected their time machine, we solve this problem by passing the handle by address: The Create­Timer­Queue­Timer function fills the address with the timer, so that the callback function can read it back out.

In practice, this additional work is no additional work at all, because you're already passing some data to the callback function, probably an object or at least a pointer to a structure. You can stash the timer handle inside that object. In our case, our object is just the handle itself. If you prefer to be more explicit:

struct ACTIONINFO
{
 HANDLE hTimer;
};

void CALLBACK ActionAfterDelayProc(void *lpParameter, BOOLEAN)
{
 ACTIONINFO *pinfo = static_cast<ACTIONINFO *>(lpParameter);
 Action();
 DeleteTimerQueueTimer(NULL, pinfo->hTimer, NULL);
 delete pinfo;
}

BOOL PerformActionAfterDelay()
{
 BOOL fSuccess = FALSE;
 ACTIONINFO *pinfo = new(std::nothrow) ACTIONINFO;
 if (pinfo != NULL) {
  if (CreateTimerQueueTimer(
     &pinfo->hTimer, NULL, ActionAfterDelayProc, pinfo,
     ACTIONDELAY, 0, WT_EXECUTEONLYONCE)) {
   fSuccess = TRUE;
  }
 }
 if (!fSuccess) {
  delete pinfo;
 }
 return fSuccess;
}

The threadpool functions were redesigned in Windows Vista to allow for greater reliability and predictability. For example, the operations of creating a timer and setting it into action are separated so that you can preallocate your timer objects (inactive) at a convenient time. Setting the timer itself cannot fail (assuming valid parameters). This makes it easier to handle error conditions since all the errors happen when you preallocate the timers, and you can deal with the problem up front, rather than proceeding ahead for a while and then realizing, "Oops, I wanted to set that timer but I couldn't. Now how do I report the error and unwind all the work that I've done so far?" (There are other new features, like cleanup groups that let you clean up multiple objects with a single call, and being able to associate an execution environment with a library, so that the DLL is not unloaded while it still has active thread pool objects.)

The result is, however, a bit more typing, since there are now two steps, creating and setting. On the other hand, the new threadpool callback is explicitly passed the PTP_TIMER, so we don't have to play any weird time-travel games to get the handle to the callback, like we did with Create­Timer­Queue­Timer.

void CALLBACK ActionAfterDelayProc(
    PTP_CALLBACK_INSTANCE, PVOID, PTP_TIMER Timer)
{
 Action();
 CloseThreadpoolTimer(Timer);
}

BOOL PerformActionAfterDelay()
{
 BOOL fSuccess = FALSE;
 PTP_TIMER Timer = CreateThreadpoolTimer(
                      ActionAfterDelayProc, NULL, NULL);
 if (Timer) {
  LONGLONG llDelay = -ACTIONDELAY * 10000LL;
  FILETIME ftDueTime = { (DWORD)llDelay, (DWORD)(llDelay >> 32) };
  SetThreadpoolTimer(Timer, &ftDueTime, 0, 0); // never fails!
  fSuccess = TRUE;
 }
 return fSuccess;
}

Anyway, that's a bit of a whirlwind tour of some of the ways of arranging for code to run after a delay.

Comments (23)
  1. WndSks says:

    Why do these APIs have threadpool in their name, why not just timerpool? Smells like a leaky abstraction.

    Thanks for posting samples for both the new and old API, I know some of us tend to nag about it.

    OT: I guess I should not vote on this post so you guys don't accuse me of being the childish down-voter :/

    [Because they are "thread pool tasks triggered by time." As opposed to "thread pool tasks triggered by signals" or "thread pool tasks that don't need a trigger". You can manage them just like any other thread pool tasks. If they were a separate timer pool, then you couldn't (say) put a timer-triggered task and a plain task in the same task group. -Raymond]
  2. @WndSks: The threads can be used for things other than timers.

  3. John says:

    The lowercase p in Threadpool really bothers me for some reason.  Seems like most other frameworks, including .NET, camel-case it.

  4. Adam Rosenfield says:

    Yo dawg, I heard you like timers, so I put a timer in your timer-queue.

  5. @WndSks: I think we can be pretty sure you aren't xpclient, because you didn't try to argue that everything is wrong and it would be all better if only someone had done it any other way.

  6. I never claimed everything MS do is wrong. I just want a backward compatible design of the UI and features of the Windows OS so the user doesn't have to make feature compromises. It was a coincidence that I changed my user name and then noticed all the commotion! Whatever I learnt about UX, I learnt from using Windows 95-XP where it was a logical evolution and then suddenly MS lost their minds and started re-imagining throwing out lots of good bits as well which is why I had to co-develop Classic Shell.

    [What is the significance of the "Msft" as the start of your handle? If you're a Microsoft employee, then you've been violating the company's social media policies all this time by not clearly identifying yourself as a Microsoft employee and not using your real name. If you're not a Microsoft employee, then you're violating this blog's rules of "no misrepresentation" (and possibly the site's ToS on false identity). -Raymond]
  7. Aleksej says:

    Doesn't examples with CreateTimerQueueTimer have a race condition: ActionAfterDelayProc can call DeleteTimerQueueTimer before timer handle is assigned to a variable in the thread calling CreateTimerQueueTimer function? I mean if the ACTIONDELAY is small and Action is fast and the originating thread is pre-empted.

  8. ChrisR says:

    @Aleksej:  Presumably CreateTimerQueueTimer is implemented in such a way that the output timer handle is set prior to the timer function being called.

  9. I didn't know that. I changed it back. (Sigh)

  10. Peter says:

    I've always found the biggest headache with delayed tasks is shutdown.  What happens in Raymond's first example when the process exits before ActionAfterDelayProc completes?  If you're lucky, then nothing.  If you're unlucky, then Action() will run right after the CRT unitializes but just before Windows kills your threads.  The resulting crash is guaranteed debugging hilarity. ;)

  11. Maurits says:

    @Peter your shutdown path needs to call DeleteTimerQueueTimer for all created timer-queue timers, or call DeleteTimerQueueEx to delete them all at once.

    But yes, close attention must always be paid to shutdown.

  12. Maurits says:

    In particular the problem with Raymond's first example is here:

    if (hThread) {

     CloseHandle(hThread);

    Now you have an orphaned thread running for a long time with no way to stop it.

    One approach is to stash the thread handle somewhere and call WaitForSingleObject(m_hThread, INFINITE) on the shutdown path, but there are others.

  13. @ChrisR:  Assume makes an ass of u and me.  Presume makes the ass look pre-tentious.  ;)

    (just making a funny – but more seriously there is always a chance that any assumption might prove false, even if it is dressed up as a presumption)  :)

    @Raymond re xpclient:  Maybe "Msft" was for "Misfit" rather than "Microsoft" ?  And how does the "misrepresentation" requirement fit alongside allowing non-actual identity ?  Presumably if xpclient is using Win 7 then he falls foul even of this clause ?  Just asking.  :)

  14. JustSomeGuy says:

    Wow, unless you have a desperate need to preserve threads, I'd be opting for the "borrow a thread from the thread pool" version. That seems to me to be the easiest solution. Options that involve me typing in 26 lines of code when 3 will do the trick give me the shivers.

  15. Drak says:

    It could be because it's early on Friday morning, but I can't for the life of me think of a scenario where I would want to do something asynchronously after a delay… Does anyone have an example?

  16. Matt says:

    @Drak: Checking to see if any non-critical updates are available for your app? You might want to let the user get started before kicking off lots of work in the background.

  17. Strangely says:

    In the timer queue timer example, I think that PerformActionAfterDelay can attempt to delete a 0 pointer if new returns 0…

  18. GregM says:

    "PerformActionAfterDelay can attempt to delete a 0 pointer if new returns 0…"

    …which is perfectly okay, though new should normally throw a std::bad_alloc exception rather than returning nullptr.

  19. Gabe says:

    JustSomeGuy: If you will only ever have *one* or *a few* timers going simultaneously, it's not a big deal. It's unlikely that you have so many threads that adding just a few more will put you over the limit.

    However, you have to remember that by default each thread will chew up 1MB of address space. That means you can only fit around 2000 of them in a standard 32-bit process, and that's only if you're not doing much else.

    If you're just waiting for your one connection to a server to time out, go for it. But if you're writing a server, it's pretty foolish to waste a thread, a couple pages of RAM, and a huge chunk of VM just so you can disconnect clients that have hung on for too long.

    It's hard to imagine something sadder than a server whose scalability is limited by the timers used to get rid of clients after their maximum connection time is up! Of course if that's your approach to timers, there's probably something else limiting your server's scalability.

  20. Strangely says:

    GregM: my understanding must be wildly out of date. How is it 'perfectly okay' to call delete on a 0 pointer? (My ten second experiment crashes, as expected.)

    [5.3.5(2) says "the value of the operand of delete may be a null pointer value." -Raymond]
  21. Strangely says:

    Thanks, especially for the reference; I don't seem to be as badly out of date as I first suspected.

  22. Drak says:

    @Matt: thank you for the example.

  23. hagenp says:

    "Since the Microsoft Research people haven't yet perfected their time machine,"

    What???? Still not???? Fire them buggers!!

    ;-)

Comments are closed.