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 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
object is initialized, you simply bracket your code with calls to CCriticalSection
and EnterCriticalSection
, passing in the LeaveCriticalSection
object by reference. CriticalSection
EnterCriticalSection (&myCriticalSection);
{
// do the work which requires synchronization
}
LeaveCriticalSection (&myCriticalSection);
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
object, often as a member variable of your client object. At the point where you need to protect your code, you create a CMutex
object and pass in the mutex by reference. You then attempt to shut the lock, by calling the function CSingleLock
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 Lock()
.Unlock()
If you specify
(zero) as the wait time, it will not wait at all. Alternatively, you can specify 0
, in which case it will wait until the Red Sox win the World Series.INFINITE
If you need to acquire access to multiple objects before you are ready to do the work, you will want to use the
. A CMultiLock
takes an array of CMultiLock
and waits on them all. CSyncObjects
takes three parameters:
CMultiLock
dwWakeMask
parameterThis 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."
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
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.PortController
The
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.CSemaphore
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
thread object for each port (CCaller
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 m_nTotalCallers
event is set, allowing CallersReadyEvent
to be acquired. Once theLock
is acquired, I iterate through the callers, telling them to begin calling.theLock
Since the callers are initialized, each in their own threads, the port manager must be sure to wait on the
to make sure it doesn't begin instructing some threads to begin calling before they are all initialized. CallersReadyEvent