Synchronizing Threads

To work with threads, you must be able to coordinate their actions. Sometimes coordination requires ensuring that certain actions happen in a specific order. Besides the functions to create threads and modify their scheduling priority, the Win32 API contains functions to make threads wait for signals from objects, such as files and processes. It also supports special synchronization objects, such as mutexes and semaphores.

The functions that wait for an object to reach its signaled state best illustrate how synchronization objects are used. With a single set of generic waiting commands, you can wait for processes, threads, mutexes, semaphores, events, and a few other objects to reach their signaled states. This command waits for one object to turn on its signal:

DWORD WaitForSingleObject( HANDLE hObject,         // object to wait for
                           DWORD dwMilliseconds ); // maximum wait time

WaitForSingleObject allows a thread to suspend itself until a specific object gives its signal. In this command, a thread also states how long it is willing to wait for the object. To wait indefinitely, set the interval to INFINITE. If the object is already available or if it reaches its signal state within the designated time, WaitForSingleObject returns 0 and execution resumes. If the interval passes and the object is still not signaled, the function returns WAIT_TIMEOUT.

WARNING

Beware when setting the interval to INFINITE. If for any reason the object never reaches a signaled state, the thread will never resume. Also, if two threads establish a reciprocal infinite wait, they will deadlock.

To make a thread wait for several objects at once, call WaitForMultipleObjects. You can make this function return as soon as any one of the objects becomes available, or you can make it wait until all the requested objects finally reach their signaled states. An event-driven program might set up an array of objects that interest it and respond when any of them signals.

DWORD WaitForMultipleObjects(
   DWORD    dwNumObjects,     // number of objects to wait for
   LPHANDLE lpHandles,        // array of object handles
   BOOL     bWaitAll,         // TRUE, wait for all; FALSE, wait for any
   DWORD    dwMilliseconds ); // maximum waiting period

Again, a return value of WAIT_TIMEOUT indicates that the interval passed and no objects were signaled. If bWaitAll is FALSE, a successful return value, which has a flag from any one element, indicates which element of the lpHandles array has become signaled. (The first element is 0, the second is 1, and so on.) If bWaitAll is TRUE, the function does not respond until all flags (all threads) have completed.

Two extended versions of the wait functions add an alert status allowing a thread to resume if an asynchronous read or write command happens to end during the wait. In effect, these functions say, “Wake me up if the object becomes available, if a certain time passes, or if a background I/O operation runs to completion.”

DWORD WaitForSingleObjectEx(
   HANDLE hObject,          // object to wait for
   DWORD  dwMilliseconds,   // maximum time to wait
   BOOL   bAlertable );     // TRUE to end wait if I/O completes
DWORD WaitForMultipleObjectsEx(
   DWORD    dwNumObjects,   // number of objects to wait for
   LPHANDLE lpHandles,      // array of object handles
   BOOL     bWaitAll,       // TRUE to wait for all; FALSE to wait for any
   DWORD    dwMilliseconds, // maximum waiting period
   BOOL     bAlertable );   // TRUE to end wait if I/O completes

Successful wait commands usually modify the awaited object in some way. For example, when a thread waits for and acquires a mutex, the wait function restores the mutex to its unsignaled state so other threads will know it is in use. Wait commands also decrease the counter in a semaphore and reset some kinds of events.

Wait commands do not modify the state of the specified object until all objects are simultaneously signaled. For example, a mutex can be signaled, but the thread does not receive ownership immediately because it is  required to wait until the other objects are also signaled; therefore, the wait function cannot modify the object. In addition, the mutex may come under the ownership of another thread while waiting, which will further delay the completion of the wait condition.

Of course, you must create an object before you can wait for it. Start with mutexes and semaphores because they have parallel API commands to create the objects, acquire or release them, get handles to them, and destroy them.

Creating Mutexes and Semaphores

The creation functions for mutexes and semaphores need to be told what access privileges you want, some initial conditions for the object, and an optional name for the object.

   LPSECURITY_ATTRIBUTES lpsa,  // optional security attributes 
   BOOL bInitialOwner           // TRUE if creator wants ownership 
   LPTSTR lpszMutexName )       // object’s name
HANDLE CreateSemaphore(
   LPSECURITY_ATTRIBUTES lpsa,  // optional security attributes 
   LONG lInitialCount,          // initial count (usually 0)
   LONG lMaxCount,              // maximum count (limits # of threads)
   LPTSTR lpszSemName );        // name of the semaphore (may be NULL)

If the security descriptor is NULL, the returned handle will possess all access privileges and will not be inherited by child processes. The names are optional; they are useful for identification purposes when several different processes want handles to the same object.

By setting the bInitialOwner flag to TRUE, a thread both creates and acquires a mutex at once. The new mutex remains unsignaled until the thread releases it.

While only one thread at a time may acquire a mutex, a semaphore remains signaled until its acquisition count reaches iMaxCount. If any more threads try to wait for the semaphore, they will be suspended until some other thread decreases the acquisition count.

Acquiring and Releasing Mutexes and Semaphores

Once a semaphore or a mutex exists, threads interact with it by acquiring and releasing it. To acquire either object, a thread calls WaitForSingleObject (or one of its variants). When a thread finishes whatever task the object synchronizes, it releases the object with one of these functions:

BOOL ReleaseMutex( HANDLE hMutex );
BOOL ReleaseSemaphore(
   HANDLE hSemaphore,
   LONG lRelease,          // amount to increment counter on release
                           // (usually 1)
   LPLONG lplPrevious );   // variable to receive the previous count

Releasing a mutex or a semaphore increments its counter. Whenever the counter rises above 0, the object assumes its signaled state, and the system checks to see whether any other threads are waiting for it.

Only a thread that already owns a mutex—in other words, a thread that has already waited for the mutex—can release it. Any thread, however, can call ReleaseSemaphore to adjust the acquisition counter by any amount up to its maximum value. Changing the counter by arbitrary amounts lets you vary the number of threads that may own a semaphore as your program runs. You may have noticed that CreateSemaphore allows you to set the counter for a new semaphore to something other than its maximum value. You might, for example, create it with an initial count of 0 to block all threads while your program initializes and then raise the counter with ReleaseSemaphore.

WARNING

Remember to release synchronization objects. If you forget to release a mutex, any threads that wait for it without specifying a maximum interval will deadlock; they will not be released.

A thread may wait for the same object more than once without blocking, but each wait must be matched with a release. This is true of mutexes, semaphores, and critical sections.

Working with Events

An event is the object a program creates when it requires a mechanism for alerting threads if some action occurs. In its simplest form—a manual reset event—the event object turns its signal on and off in response to the two commands SetEvent (signal on) and ResetEvent (signal off).  When the signal is on, all threads that wait for the event will receive it. When the signal is off, all threads that wait for the event become blocked. Unlike mutexes and semaphores, manual reset events change their state only when some thread explicitly sets or resets them.

You might use a manual reset event to allow certain threads to execute only when the program is not painting its window or only after the user enters certain information. Here are the basic commands for working with events:

HANDLE CreateEvent(
   LPSECURITY_ATTRIBUTES lpsa, // security privileges (default = NULL)
   BOOL bManualReset,          // TRUE if event must be reset manually
   BOOL bInitialState,         // TRUE to create event in signaled state
   LPTSTR lpszEventName );     // name of event (may be NULL)
BOOL SetEvent( HANDLE hEvent );
BOOL ResetEvent( HANDLE hEvent );

Using the bInitialState parameter, CreateEvent allows the new event to arrive in the world already signaled. The SetEvent and ResetEvent functions return TRUE or FALSE to indicate success or failure.

With the bManualReset parameter, CreateEvent lets you create an automatic reset event instead of a manual reset event. An automatic reset event returns to its unsignaled state immediately after a SetEvent command. ResetEvent is redundant for an automatic reset event. Furthermore, an automatic reset event always releases only a single thread on each signal before resetting. An automatic reset event might be useful for a program where one master thread prepares data for other working threads. Whenever a new set of data is ready, the master sets the event and a single working thread is released. The other workers continue to wait in line for more assignments.

Besides setting and resetting events, you can pulse events.

BOOL PulseEvent( hEvent );

A pulse turns the signal on for a very short time and then turns it back off. Pulsing a manual event allows all waiting threads to pass and then resets the event. Pulsing an automatic event lets one waiting thread pass and then resets the event. If no threads are waiting, none will pass. Setting an automatic event, on the other hand, causes the event to leave its signal on until some thread waits for it. As soon as one thread passes, the event resets itself.

NOTE

The NamedPipe demo, discussed in Chapter 15, demonstrates the use of automatic and manual reset events.

Sharing and Destroying Mutexes, Semaphores, and Events

Processes—even unrelated ones—can share mutexes, semaphores, and events. By sharing objects, processes can coordinate their activities, just as threads do. There are three mechanisms for sharing. One is inheritance, where one process creates another and the new process receives copies of the parent’s handles. Only those handles marked for inheritance when they were created will be passed on.

The other methods involve calling functions to create a second handle to an existing object. Which function you call depends on what information you already have. If you have handles to both the source and destination processes, call DuplicateHandle. If you have only the name of the object, call one of the Open functions. Two programs might agree in advance on the name of the object they share, or one might pass the name to the other through shared memory, DDEML (DDE Management Library), or a pipe.

BOOL DuplicateHandle(
   HANDLE hSourceProcess, // process that owns the original object
   HANDLE hSource,        // handle to the original object
   HANDLE hTargetProcess, // process that wants a copy of the handle
   LPHANDLE lphTarget,    // place to store duplicated handle
   DWORD    fdwAccess,    // requested access privileges
   BOOL     bInherit,     // may the duplicate handle be inherited?
   DWORD    fdwOptions ); // optional actions, e.g., close source handle
HANDLE OpenMutex(
   DWORD fdwAccess,       // requested access privileges
   BOOL bInherit,         // TRUE if children may inherit this handle
   LPTSTR lpszName );     // name of the mutex
HANDLE OpenSemaphore(
   DWORD fdwAccess,       // requested access privileges
   BOOL bInherit,         // TRUE if children may inherit this handle
   LPTSTR lpszName );     // name of the semaphore
HANDLE OpenEvent(
   DWORD fdwAccess,       // requested access privileges
   BOOL bInherit,         // TRUE if children may inherit this handle
   LPTSTR lpszName );     // name of the event

NOTE

By the way, those LPTSTR variable types are not a misprint. It’s a generic text type that compiles differently depending on whether an application uses Unicode or ASCII strings.

Mutexes, semaphores, and events persist in memory until all the processes that own them end or until all the object’s handles have been closed with CloseHandle.

BOOL CloseHandle( hObject );

Working with Critical Sections

A critical section object performs exactly the same function as a mutex except that critical sections may not be shared. They are visible only within a single process. Critical sections and mutexes both allow only one thread to own them at a time, but critical sections work more quickly and involve less overhead.

The functions for working with critical sections do not use the same terminology as the functions for working with mutexes, but they do roughly the same things. Instead of creating a critical section, you initialize it. Instead of waiting for it, you enter it. Instead of releasing it, you leave it. Instead of closing its handle, you delete the object.

VOID InitializeCriticalSection( LPCRITICAL_SECTION lpcs );
VOID EnterCriticalSection( LPCRITICAL_SECTION lpcs );
VOID LeaveCriticalSection( LPCRITICAL_SECTION lpcs );
VOID DeleteCriticalSection( LPCRITICAL_SECTION lpcs );

The variable type LPCRITICAL_SECTION names a pointer (not a handle) to a critical section object. InitializeCriticalSection expects to receive a pointer to an empty object, &cs , which you can allocate like this:

CRITICAL_SECTION cs;

© 1998 SYBEX Inc. All rights reserved.