30.2SynchronizationObjects 467
version of semop(), called semtimedop(), which can be used for the remaining
timeout scenario. Our implementation does not use
semtimedop() in order to
maintain a high level of portability. Instead, we poll the semaphore at a prede-
fined interval using the
nanosleep() function to put the thread to sleep between
attempts. Of course, this is not the best solution and, if
semtimedop() is availa-
ble on your platform, you should favor using it.
WaitObjects
Now that we have the basic implementation of our synchronization mechanism,
we have to extend our objects in order to add state recording functionality. To do
so, we introduce a new class called
WaitObject that implements state recording
using the wait state array from our
ThreadInfo class. This array is a simple inte-
ger array with a constant size, and it belongs to exactly one thread. Because only
the owner thread is allowed to modify the content of the array, we do not have to
use any synchronization during state recording.
The format of a single state record is shown in Table 30.2. The first field
specifies the current state of the object, which can be either
STATE_WAIT or
STATE_LOCK. The next two fields specify the time at which the object entered the
current state and, in case of a wait operation, the specified timeout. The object
count field defines how many object IDs are to follow, which might be more than
one in case of a
WaitForMultipleObjects() operation. Finally, the last field
specifies the total size of the record in integers, which in most cases is six. You
might wonder why we put the state size field at the end and not at the beginning
of a record. In most cases, we remove a record from the end of our wait
state array, and it therefore makes sense that we start our search from the end of
the integer array, which means the state size field is actually the first element in
our record.
In order to set an object’s state to wait, we use the
SetStateWait() function
from our
WaitObject class, which simply fills the array with the required infor-
mation. If an object’s state becomes locked, then we need to call the
SetStateLock() method. This method requires a parameter that specifies
whether the object immediately entered the locked state or whether it made a
Field State Timestamp Timeout Object
Count
Object IDs State
Size
Integers 1 1 1 1 1 – object count 1
Table 30.2. Wait state record.
468 30.ACrossPlatformMultithreadingFramework
state transition from a previous wait. In the first case, again we can simply fill the
array with the lock state information. However, in the second case, we have to
find the wait state record of our object in the wait state array, change the state to
locked, and set the timestamp value. Thanks to the state size field, scanning the
array for the right entry can be done very quickly.
RemoveStateWait() and Re-
moveStateLock()
work in a similar fashion. First, we search for the wait or lock
entry of the current object in the array. Then we delete it by setting all fields to
zero. Of course, it might happen that the entry is not the last in the array, in
which case we have to relocate all of the following entries.
WaitObject() also defines four important abstract methods that need to be
implemented by derived classes:
Lock(), Unlock(), LockedExtern(), and Un-
lockedExtern()
. We provide classes derived from WaitObject for each of our
basic synchronization objects. Each class implements
Lock() and Unlock() by
forwarding the call to the underlying object and setting the according wait and
lock states.
LockedExtern() and UnlockedExtern() are called if a Wai-
tObject
’s internal synchronization object is locked or unlocked outside the
class. These methods are helper functions used by our
WaitForMultipleOb-
jects()
implementation in order to keep each WaitObject’s state correctly up-
to-date. In order to prevent someone else from messing up our wait states with
these methods, we declared them private and made the
WaitList class a friend of
WaitObject.
With the recorded wait state information, our
ThreadManager is now able to
construct a complete snapshot of the current program state as in the example
shown in Listing 30.4. The output shows us that we have three different threads.
We start with the first one with the ID 27632, which is our main program thread.
Because our
ThreadManager did not create this thread but only attached itself to
it, no thread name is given. We can see that our main thread owns a lock (since
Thread WaitObject Information:
27632 : Attached Thread
- Locked (500ms) wait object 1013 : WaitMutex
28144 : WaitThread
- Waiting (500ms/INFINITE) for multiple wait objects (2)
- 1012 : WaitEvent
- 1013 : WaitMutex
28145 : DumpThread
- No Locks
Listing 30.4. Example dump of wait state information.
30.2SynchronizationObjects 469
the timestamp is 500 ms) on a wait object with ID 1013 named WaitMutex.
WaitThread is the name of the next thread in the list, and this thread is blocked
in a
WaitForMultipleObjects() call. It waits for an event named WaitEvent
with ID 1012, and it waits for the same mutex that our main thread has locked.
We can also see that we have already been waiting for 500 ms and that the speci-
fied timeout value is infinite. Finally, the last thread in the list is called
Dump-
Thread
. This is the thread that was used to output the status information, and it
has neither waits nor locks.
LockHelperClasses
An important practice when programming in multithreaded environments is to
release object ownership once a thread is done with its work. Of course, owner-
ship must also be released in case of an error, which is actually not that easy.
Common pitfalls include functions that do not have a single point of exit and un-
handled exceptions. To better illustrate this problem, take a look at the code in
Listing 30.5, which contains one obvious error where it fails to release the mutex
lock. In the case that an error occurs, we simply return false but forget to release
our lock. While this issue can be easily fixed by adding the missing
Unlock()
call before the
return statement, we still have another error hiding in the sample
code. Imagine that somewhere between the
Lock() and Unlock() calls, an ex-
ception occurs. If this exception does not occur within a try-catch block, we exit
the function, leaving the mutex in a locked state.
A simple and elegant solution to this problem is to provide RAII-style
3
lock-
ing using a little helper class. Our framework offers two different versions of this
class called
LockT and WaitLock. LockT is a simple template class that works
with all synchronization objects that implement a
Lock() and Unlock() method.
On construction, this class receives a reference to the guarded synchronization
object and, if requested, immediately locks it.
LockT() also offers explicit
Lock() and Unlock() functions, but the most important feature is that it unlocks
a locked object in its destructor. Listing 30.6 shows how to use
LockT() to fix all
issues in Listing 30.5.
The
WaitLock class implementation is almost identical to LockT’s—the only
difference is that it only works with synchronization objects derived from
Wai-
tObject
. There are two reasons why we do not use LockT for WaitObjects. The
first is that these objects are reference counted and need special handling when
passing a raw pointer to our helper class. The other reason is that
WaitObjects
3
RAII stands for resource acquisition is initialization. See Stroustrup, The Design and
Evolution of C++, Addison-Wesley, 1994.
470 30.ACrossPlatformMultithreadingFramework
BasicMutex mtx;
mtx.Lock();
... // do some work here
if (errorOccured) return (false);
mtx.Unlock();
return (true);
Listing 30.5. A bad example for using the mutex class.
BasicMutex mtx;
LockT<BasicMutex> mtxLock(mtx, true); // true means lock the Mutex
... // do some work here
if (errorOccured) return (false);
return (true);
Listing 30.6. Example for using a mutex with the LockT helper class
introduce a certain overhead due to the additional wait state recording. Therefore,
you should carefully choose what synchronization object type to use when per-
formance matters.
WaitingforMultipleObjects
A nice feature on Windows platforms is the ability to wait for several different
objects to enter the signaled state. These objects can be synchronization objects
(mutex, semaphore, event), as well as other object types like sockets or threads.
Unfortunately, the POSIX standard doesn’t specify a similar API. In order to
provide the same functionality on UNIX platforms, we introduce the concept of
wait lists. A
WaitList is a simple container that holds an array of WaitObjects
that we want to wait for. It provides two wait methods,
WaitForSingleObject()
and
WaitForMultipleObjects(), where the first method waits for a specific
wait object to enter the signaled state, and the second method waits for any object
in the list to become signaled. On Windows platforms, we can simply call the
corresponding Windows API functions,
WaitForSingleObject() and WaitFor-
MultipleObjects()
. However, our UNIX implementation is a bit more
complex.
30.3Limitations 471
Figure 30.1. WaitForMultipleObjects() states.
Of course, the easiest solution is to simply poll our wait object list and try to
lock each object with a zero timeout (nonblocking). If one of our attempts suc-
ceeds, then we can return true immediately. Otherwise, if all attempts fail, then
we put our thread to sleep for a short period. This solution is not the most elegant
one since we keep waking up the thread and polling the wait objects even though
none of their states have changed. Therefore, our implementation uses a different
approach, slightly based on [Nagarajayya and Gupta 2000].
Figure 30.1 shows the different states and the respective transitions in our
WaitForMultipleObjects() implementation. We start off by trying to lock one
of the wait objects in our list. If this step is not successful, then we put our thread
to sleep, waiting on a condition variable. We are using a single global condition
variable that notifies us whenever a synchronization object has changed its state
to signaled. Every synchronization object in our framework sets this variable
when its
Unlock() or Reset() method is called. Only if such a state transition
has occurred do we go through our list of wait objects again and try to lock each
one. The advantage of this solution is that it only resumes the thread and
checks whether a lock can be obtained after an object has actually changed to a
signaled state. The overhead of our solution is also relatively small because we
only use one additional condition variable in the whole framework. Of course, it
can still happen that the thread performs unnecessary checks in the case that an
object is unlocked but is not part of the
WaitList. Therefore, this function
should be used with caution on UNIX systems.
30.3Limitations
Now that we have provided an overview of our threading framework and its fea-
tures, it is time to discuss some limitations and potential pitfalls. The first thing
Wait for
signal
Try lock Exit
Lock success
TimeoutLock failed
Signal
received
Lock()
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.17.176.72