The MFC Perspective

The Microsoft Foundation Classes (MFC) supports four synchronization objects: critical sections, mutexes, semaphores and events. The MFC also supports two types of locks: single locks and multi-locks.

The details of the implementation of the locks and even of the synchronization objects are encapsulated in the objects and opaque to the application developer. You can build your application without worrying about how the locks are implemented; as long as you follow the correct protocol, MFC will ensure synchronization.

Critical Sections

Critical sections are the simplest form of MFC synchronization objects. They are easy to use and they are "lightweight" in that they don't have a lot of overhead and hence they are very fast. You can provide synchronized access to a section of code by marking it as a critical section. Once the

CCriticalSection
object is initialized, you simply bracket your code with calls to
EnterCriticalSection
and
LeaveCriticalSection
, passing in the
CriticalSection
object by reference.

EnterCriticalSection (&myCriticalSection);

{

// do the work which requires synchronization

}

LeaveCriticalSection (&myCriticalSection);

Mutexes

Critical sections are fast and easy to use, but they have no features. You cannot set your code to "time out" waiting for a critical section, the code will just block and wait forever. You also cannot have two processes wait on the same critical section as there is no way to "name" a critical section so that it appears in both processes. For either of these features, you need to step up to a mutex.

Mutex stands for Mutually Exclusive, and provides exclusive access to a section of code, much like a critical section does. The typical use of a mutex is somewhat different, however.

CSingleLock theLock ( &myMutex );

if ( theLock.Lock(WAIT_TIME) )

{

// do the work which requires synchronization

theLock.Unlock();

}

else

{

// handle the failure to lock

}

You start by declaring a

CMutex
object, often as a member variable of your client object. At the point where you need to protect your code, you create a
CSingleLock
object and pass in the mutex by reference. You then attempt to shut the lock, by calling the function
Lock()
and passing in as a parameter the amount of time you are willing to wait (in milliseconds). Either you will acquire the lock or you will time out. If you time out, you must handle that situation yourself. If you get the lock, you do the work and then release the lock by calling
Unlock()
.

If you specify

0
(zero) as the wait time, it will not wait at all. Alternatively, you can specify
INFINITE
, in which case it will wait until the Red Sox win the World Series.

If you need to acquire access to multiple objects before you are ready to do the work, you will want to use the

CMultiLock
. A
CMultiLock
takes an array of
CSyncObjects
and waits on them all.
CMultiLock
takes three parameters:

This latter parameter controls whether the multilock should return on events other than those for which you are waiting. For example, you can set it to return if it receives a point message, a timer message, a hot key message and so forth. Typically it is set to zero, which means, "Just wait for the objects I told you about."

Semaphores

MFC uses the term semaphore in an unusual way. In the MFC, semaphore refers to a specific variety of synchronization objects — one that is used to manage access to a limited set of resources. For example, if you have five ports available for output and multiple threads which may want access to these ports, you may want to design a

PortController
object, which would be responsible for allocation of ports to waiting threads. This allocation must be made thread-safe, but you don't want to block out any thread until you've allocated all the ports. Furthermore, as ports become available, you want to be able to allocate them to waiting threads.

The

CSemaphore
object will keep count of how many ports you have, and how many are in use. It will release waiting threads when there is a port available, and otherwise allow them to wait.

Events

In some circumstances, you may want your thread to wait until other threads have accomplished a certain task, or some other UI event is registered. You may want to initialize a number of resources, and when they are all fully registered, go forward and interact with these resources. As an example, imagine you have a number of outgoing telephone ports connected to your machine. Each port will be controlled by a single thread, so that if you have 72 such ports there are 72 threads. You also have a port manger thread, which is responsible for spinning up the 72 threads and keeping track of their status. This port manager may want to initialize all the threads before any calls are made on any port.

// For the total count of outbound ports…

for (i = 0; i<m_nTotalCallers; i++)

{

// ... (preliminary set up)

// create the threads

CCaller * pCaller = new CCaller(// parameters);

BOOL bRc = pCaller->CreateThread();

// ... (other book keeping work)

}

CSingleLock theLock ( &m_CallersReadyEvent );

if (theLock.Lock(WAIT_VERY_LONG_TIME))

{

for (int j = 0; j < m_nTotalCallers; j++)

{

// set them calling

}

theLock.Unlock();

}

else// never got event!

{

// handle error

}

In this pseudo-code, I create a

CCaller
thread object for each port (
m_nTotalCallers
holds the number of ports). This calls the initialization routine within the thread. That initialization routine signals back to the port manager that it is complete. When all the ports have said they are complete, the
CallersReadyEvent
event is set, allowing
theLock
to be acquired. Once
theLock
is acquired, I iterate through the callers, telling them to begin calling.

Since the callers are initialized, each in their own threads, the port manager must be sure to wait on the

CallersReadyEvent
to make sure it doesn't begin instructing some threads to begin calling before they are all initialized.

© 1998 by Wrox Press. All rights reserved.