Worker Threads

Waiting for a thread to start

I recently processed a newsgroup message which was discussing the problem of waiting for thread startup. The proposed mechanism looked like this:

BOOL waiting; // flag (this is one bug right here!)
void CMyClass::OnStartThread()
   {
    waiting = TRUE;
    AfxBeginThread(myThread, something);
    while(waiting) /* spin */ ;   // horrible!
   }

UINT CMyClass::myThread(LPVOID whatever) // *** static *** member function
   {
    ... initialization sequence here
    waiting = FALSE; // allow initiatior to continue (probably won't work)
    ... thread computations
    return 0;
   }

This code has several problems. First, it is a terrible implementation; it requires that the parent thread run until its timeslice ends, which means that it delays the startup of the created thread. This alone is enough reason to throw the code out. But even if it were acceptable, it is still wrong. The waiting variable must be declared as volatile, because under the optimizing compiler, the while loop which spins may or may not be executed, and if it is executed, it will probably never exit. I discuss this in more detail in my essay on "Surviving the Release Build". But the bottom line is that when you have a variable of any sort that can be modified from one thread and whose modification must be detected in another, you must declare it as volatile.

The busy-wait is the serious disaster here. Since a timeslice, or quantum, is 200 ms in NT, it means that the thread can waste up to 200 ms, doing nothing, before allowing the spawned thread to run. If the spawned thread blocks on something like an I/O operation, and control returns to the creator, each time control returns to the creator, it will burn up another 200ms (the kernel doesn't care that it is doing nothing but polling a Boolean variable that can never change while it is polling it on a uniprocessor; it only knows that the thread is executing). As you can see, it doesn't take very many I/O operations in the thread to add many seconds to the perceived thread startup time.

The correct solution is shown below. In this solution, a manual-reset Event is used. To simplify the code, we create it just before it is needed, and destroy it immediately afterwards; in the case where the thread may be started up several times, the optimization to move this out to a member variable should be obvious. Note that doing this as a member variable suggests that the Event would be created in the class's constructor and destroyed in its destructor.

class CMyClass : public CView { // or something like this...
     protected:
         HANDLE startupEvent;
 };

void CMyClass::OnStartThread()
   {
     startupEvent = ::CreateEvent(NULL, // no security attributes
                                  TRUE, // manual-reset
                                  FALSE,// initially non-signaled
                                  NULL);// anonymous
     AfxBeginThread(myThread, this);
     switch(::WaitForSingleObject(startupEvent, MY_DELAY_TIME))
         { /* waitfor */
          case WAIT_TIMEOUT:
              ... report problem with thread startup
              break;
          case WAIT_OBJECT_0:
              ... possibly do something to note thread is running
              break;
         } /* waitfor */
     CloseHandle(startupEvent);
     startupEvent = NULL; // make nice with handle var for debugging
   } 

UINT CMyClass::myThread(LPVOID me)
   {
    CMyClass * self = (CMyClass *)me;
    self->run;
    return 0;
   }

void CMyClass::run( )
   {
    ... long initialization sequence
    ::SetEvent(staruptEvent);
    ... loop computations
   }

Note that I haven't done anything about dealing with the fact that the startup timeout may be incorrect and the thread is still trying to start; this could be handled by, for example, attempting to ::WaitForSingleObject on the thread handle with a wait time of 0; if it times out, the thread is running; if it returns with WAIT_OBJECT_0 the thread has halted. This requires that you deal with the issues of the CWinThread object possibly being deleted before you can get the handle. No, I'm not going to try to write every possible line of code.

Actually, it is rare that I would do something like this. I'd be more inclined to use messages posted to the main window to establish state for the GUI: the thread is starting, the thread has started, the thread has terminated (note that it may be terminated without being started). This avoids the issues about the GUI blocking until the thread has actually completed the startup sequence, or dealing with the timeout issue if the thread died somehow before doing the ::SetEvent.

void CMyClass::OnStartThread( )
   {
    AfxBeginThread(myThread, this);
    PostMessage(UWM_THREAD_STARTING);
   }

UINT CMyClass::myThread(LPVOID me) // *** static *** member function
   {
    CMyClass * self = (CMyClass *)me;
    self->run( );
    return 0;
   }

void CMyClass::run( )
   {
    ... lengthy startup sequence
    PostMessage(UWM_THREAD_STARTED);
    ... long thread computation
    PostMessage(UWM_THREAD_STOPPING);
    ... long thread shutdown
    PostMessage(UWM_THREAD_TERMINATED);
   }

I use the above paradigm in many contexts. Note that it completely eliminates the need for synchronization, but adds some complexity to the GUI. For example, imagine that I have in the GUI (in this case, since I'm posting to the view, it is view-specific state) a member variable that encodes the current state: terminated-or-never-started, starting, stopping, and running. I might have menu items called Start Computation, Pause Computation, Cancel Computation. I would create ON_UPDATE_COMMAND_UI handlers that responed as follows:

void CMyClass::OnUpdateStart(CCmdUI * pCmdUI)
   {
    pCmdUI->Enable(threadstate == MY_THREAD_STOPPED);
   }
void CMyClass::OnUpdatePause(CCmdUI * pCmdUI)
   {
    pCmdUI->Enable(threadstate == MY_THREAD_RUNNING);
   }
void CMyClass::OnUpdateCancel(CCmdUI * pCmdUI)
   {
    pCmdUI->Enable(threadstate == MY_THREAD_RUNNING);
   }

Providing I didn't really need to wait (and I find that I rarely do), I have now avoided the need to introduce a blocking synchronization event in the main GUI thread, which could potentially lock up the GUI. Note also that I might change the Cancel case to allow for the thread to be cancelled even if it is the middle of starting, providing that this makes sense in the context of the thread computation. In this case, I'd have to "poll" the cancel flag during the startup, for example, by splitting out the startup into a separate function:

void CMyClass::run( )
   {
    BOOL byCancel = FALSE;
    if(!startMe( ))
     { 
      PostMessage(UWM_THREAD_STOPPED, TRUE); // stopped by cancel
      return;
     }
    PostMessage(UWM_THREAD_STARTED);
    while(!cancelFlag)
      { /* thread loop */
    ...lengthy thread computation
      } /* thread loop */
    
    byCancel = cancelFlag;
    PostMessage(UWM_THREAD_STOPPING);
    ...lengthy thread shutdown
    PostMessage(UWM_THREAD_STOPPED, byCancel);
   }

BOOL CMyClass::startMe( )
   {
    ...do something
    if(cancelFlag)
      return FALSE;
    ...open the file on the server
    if(cancelFlag)
       {
        ...close the file on the server
        return FALSE;
       }
    ... more stuff, following above idea
    return TRUE;
   }

As usual in such cases, it is important to undo everything you have done if you detect the CancelFlag has been set during startup. I've also defined WPARAM of the message to include a flag that indicates if the thread stopped because it stopped normally or stopped because the user cancelled it (which I might use to display in the log that the thread was stopped by user request). I might also extend this to a set of enumerated types to communicate back error codes in case the thread decided to terminate because of some problem. I might even use LPARAM to hold the ::GetLastError code. You see, there are many themes-and-variations of this basic scheme.

You might also like...

Comments

Contribute

Why not write for us? Or you could submit an event or a user group in your area. Alternatively just tell us what you think!

Our tools

We've got automatic conversion tools to convert C# to VB.NET, VB.NET to C#. Also you can compress javascript and compress css and generate sql connection strings.

“In theory, theory and practice are the same. In practice, they're not.”