The importance of passing the WT_EXECUTELONGFUNCTION flag to QueueUserWorkItem

Raymond Chen

One of the flags to the QueueUserWorkItem function is WT_EXECUTELONGFUNCTION. The documentation for that flag reads

The callback function can perform a long wait. This flag helps the system to decide if it should create a new thread.

As noted in the documentation, the thread pool uses this flag to decide whether it should create a new thread or wait for an existing work item to finish. If all the current thread pool threads are busy running work items and there is another work item to dispatch, it will tend to wait for one of the existing work items to complete if they are “short”, because the expectation is that some work item will finish quickly and its thread will become available to run a new work item. On the other hand, if the work items are marked WT_EXECUTELONGFUNCTION, then the thread pool knows that waiting for the running work item to complete is not going to be very productive, so it is more likely to create a new thread.

If you fail to mark a long work item with the WT_EXECUTELONGFUNCTION flag, then the thread pool ends up waiting for that work item to complete, when it really should be kicking off a new thread. Eventually, the thread pool gets impatient and figures out that you lied to it, and it creates a new thread anyway. But it often takes a while before the thread pool realizes that it’s been waiting in vain.

Let’s illustrate this with a simple console program.

#include <windows.h>
#include <stdio.h>

DWORD g_dwLastTick;

void CALLBACK Tick(void *, BOOLEAN) { DWORD dwTick = GetTickCount(); printf(“%5d\n”, dwTick – g_dwLastTick); }

DWORD CALLBACK Clog(void *) { Sleep(4000); return 0; }

int __cdecl main(int argc, char* argv[]) { g_dwLastTick = GetTickCount(); switch (argc) { case 2: QueueUserWorkItem(Clog, NULL, 0); break; case 3: QueueUserWorkItem(Clog, NULL, WT_EXECUTELONGFUNCTION); break; } HANDLE hTimer; CreateTimerQueueTimer(&hTimer, NULL, Tick, NULL, 250, 250, 0); Sleep(INFINITE); return 0; }

This program creates a periodic thread pool work item that fires every 250ms, and which merely prints how much time has elapsed since the timer was started. As a baseline, run the program with no parameters, and observe that the callbacks occur at roughly 250ms intervals, as expected.

  251
  501
  751
 1012
^C

Next, run the program with a single command line parameter. This causes the “case 2” to be taken, where the “Clog” work item is queued. The “Clog” does what its names does: It clogs up the work item queue by taking a long time (four seconds) to complete. Notice that the first callback doesn’t occur for a whole second.

 1001
 1011
 1021
 1021
 1252
 1502
 1752
^C

That’s because we queued the “Clog” work item without the WT_EXECUTELONGFUNCTION flag. In other words, we told the thread pool, “Oh, don’t worry about this guy, he’ll be finished soon.” The thread pool wanted to run the Tick event, and since the Clog work item was marked as “fast”, the thread pool decided to wait for it and recycle its thread rather than create a new one. After about a second, the thread pool got impatient and spun up a new thread to service the now-long-overdue Tick events.

Notice that as soon as the first Tick event was processed, three more were fired in rapid succession. That’s because the thread pool realized that it had fallen four events behind (thanks to the clog) and had to fire the next three immediately just to clear its backlog. The fifth and subsequent events fire roughly on time because the thread pool has figured out that the Clog really is a clog and should be treated as a long-running event.

Finally, run the program with two command line parameters. This causes the “case 3” to be taken, where we queue up the Clog but also pass the WT_EXECUTELONGFUNCTION flag.

  251
  511
  761
 1012
^C

Notice that with this hint, the thread pool no longer gets fooled by the Clog and knows to spin up a new thread to handle the Tick events.

Moral of the story: If you’re going to go wading into the thread pool, make sure you play friendly with other kids and let the thread pool know ahead of time whether you’re going to take a long time. This allows the thread pool to keep the number of worker threads low (thus reaping the benefits of thread pooling) while still creating enough threads to keep the events flowing smoothly.

Exercise: What are the consequences to the thread pool if you create a thread pool timer whose callback takes longer to complete than its timer period?

0 comments

Discussion is closed.

Feedback usabilla icon