Errata - Last Updated April 5, 2005
This page contains a list of errors, ommissions, and other
notes regarding the first edition of
Win32 Multithreaded Programming,
ISBN 1-56592-296-4, published by
O'Reilly & Associates.
What's new as of April 4, 2005:
-
Uploaded new source
that includes the following repairs:
-
Fixed a bug in the CMclLinkedListNode class (used internally
by all of the list-related classes) that caused skipped destruction of objects
that were placed on the list. The fix was to give the CMclLinkedListNode class
a virtual destructor (see CMclLinkedLists.h).
Thanks to Robin Hilliard for reporting the leak he was seeing, and for doggedly
convincing me that it was a bug in our code and not his.
What's new as of June 23, 2002:
-
Uploaded new source
that includes the following repairs:
-
Fixed a bug in the CMclMailbox class (GetAlertable and
PostAlertable). Both methods were testing the return value of
CMclWaitSucceededIndex incorrectly.
Thanks to David Gravereaux for finding and taking the time to report this
subtle bug.
What's new as of November 07, 2001:
-
Uploaded new source
that includes the following repairs:
-
Fixed a bug in the CMclMailbox class where the controlling
semaphores being used were erroneously released in the event of a timeout while
waiting for ownership of the mutex protecting things. In this situation, while
no item was posted/fetched from the mailbox, the semaphore would still be
released.
Thanks to Johan Vanslembrouck for finding and taking the time to report this
subtle bug.
What's new as of June 02, 2000:
-
Uploaded new source
that includes the following repairs:
-
Libary projects now allow batch builds of release, debug,
unicode release, and unicode debug variations.
Chapter 1. Introduction
Chapter 2. Thread Scheduling and
Basic Concepts
Chapter 3. Process and Thread
APIs
Chapter 4. Thread
Synchronization Tools
Chapter 5. Interthread
Communication
Chapter 6. Mcl: A C++ Class
Library for Multithreaded Programming
Chapter 7. Mcl: Higher-Level
Classes
Chapter 8: Basic Thread
Synchronization
Chapter 9: Advanced
Encapsulation Techniques
Chapter 10: Advanced Thread
Management Techniques
Chapter 11: DLLs in
Multithreaded Programs
Chapter 12: Multithreaded User
Interfaces and MFC
Chapter 13: Basic Multithreaded
GUI Design
Chapter 14: Structured Exception
Handling
Chapter 15: Debugging
Multithreaded Programs
Appendix A: Mcl C++ Class
Library Reference
Appendex B: Mcl4Mfc C++ Class
Library Reference
Source Code
Other Items of Note
-
Page 47. The following statement in the last paragarph of
this page is incorrect: This technique [using CREATE_SUSPENDED with CreateProcess]
is generally used by a parent process that wants to assign the STDIN or STDOUT
handles for the child process to refer to a pipe or other kernel object. By
creating the process so that the primary thread is initially suspended, the
parent process can safely use the SetStdHandle function to redirect one or more
of the standard input or output handles for the child process before the
primary thread has a chance to execute.
In fact, by the time the parent process returns from calling CreateProcess,
the handle table for the child process is completely initialized, including
those entries used for stdin, stdout, and stderr. To
modify the STD handle(s) of a child process, the parent process must replace
it's own STD handle(s) using SetStdHandle, launch the child process using
CreateProcess and the bInheritHandles parameter set to TRUE, then
restore the parent STD handles to their original state. Alternatively, the
STARTUPINFO field members hStdInput, hStdOutput, and/or hStdError
may be used to setup the STD handles of a child process that is being started.
-
Page 84. The documentation of the ReleaseMutex function
incorrectly states that if a thread owning a mutex exits or is terminated
without releasing ownership of the mutex first, all other threads waiting for
ownership of that mutex will see a return value of WAIT_ABANDONED from their
call to WaitForSingleObject. In fact, only one thread will wake up and
see a return value of WAIT_ABANDONED. That thread now owns the mutex, and is
responsible for releasing it using the ReleaseMutex function. This
condition should be considered a design flaw in your program and should be
ASSERTed and addressed as soon as this behavior is discovered.
-
None of our class are intended to support assignment of one
Mcl object to another. However, we didn't take steps to explicitly disallow
this usage model, so you will see strange results if you attempt to copy one
Mcl object instance to another, either using copy constructor or assignment
syntax. A future version of the Mcl class library will explicitly disallow this
usage model, or implement the assignment operator and a copy constructor where
these operations make sense.
-
Page 193; CMclLinkedList implementation. The
internal implementation of the CMclLinkedList class uses an event
called m_cNotEmpty (page 195) to keep track of whether or not there
are any nodes on the list. This results in a race condition in the GetFromHeadOfList
member function. The result of the race condition is that it is possible for a
thread to finish waiting on m_cNotEmpty, thinking that the list now
contains at least one node on it, when in fact it doesn't and the list is
empty.
Instead, a semaphore tracking the number of available nodes on the list should
have been used. This error has been fixed and will appear in a future printing
of the book.
-
Page 193; CMclLinkedList class. A manual reset
event, m_cNotEmpty, is used internally to keep track of whether or not
there are any nodes on the list. This can result in a race condition involving
two or more threads that are calling either GetFromHeadOfList or GetFromTailOfList.
If the list is empty, these threads will be blocked waiting for m_cNotEmpty
to be signaled by a call to PutOnHeadOfList or PutOnTailOfList.
If one node is placed on the list using either of the Put functions, it's
possible for both waiting threads to wake up from their wait on m_cNotEmpty,
thinking that there will be a node on the list for them to consume. However,
only one node was placed on the list, resulting in a race to see which thread
will get to consume that node. Since a critical section is then used to protect
the list manipulation, one of the waiting threads will successfully remove the
newly added node. The other thread will end up trying to retrieve the data out
of the master (sentinel) node, which is an invalid operation.
Instead of a manual reset event, a semaphore should have been used to track
whether or not the list was empty. This problem has been fixed and will appear
in newer printings of the book. You can also download the
revised code below.
-
Page 287; Multiple Readers/Multiple Writers. The seat
database class does not proactively prevent starvation by either readers or
writers. Writer starvation could occur if there was a sustained flurry of
reader activity. Likewise, reader starvation could occur if there was a
sustained flurry of writer activity, although in practice, this is less likely
to occur than writer starvation. In our example, these scenarios are unlikely,
so the design of the sample code doesn't prevent this type of situation. If
this scenario is likely to occur in a real life situation, the issue of
starvation would have to be dealt with. One option would be to use the monitor
synchronization object instead, which would give you total control over
granting readers and writers access to the database in a "fairer" manner.
-
Page 311; paragraph beginning "The actual CDPQueue1 class is
defined next.". Next to last sentence says "The variable m_bRun is
used to prevent the worker thread from exiting when there are always jobs in
the queue." This sentence should read "The variable m_bRun is used to
allow the worker thread to exit even if there are still jobs in the queue."
Note that the comment in the accompanying source code is correct. The loop will
exit if either the m_ceControl event is signalled or
m_bRun is set by the Stop method.
-
Page 341; Example 10-3: CFixedThreadPool class
implementation. The wrong code appears here in the book. Instead of the
implementation of CFixedThreadPool, the test code (which also appears
in Example 10-4) is shown here. The implementation of CFixedThreadPool
can be found on the CD in
Chapter10\ThreadPools\FixedThreadPool\CFixedThreadPool.cpp.
-
Although there is no hard wired limit to the number of
threads that can be created in a process, there is an effective limit by virtue
of the fact that each process has a 2 gigabyte private virtual address space
limit. When a new thread is created, the operating system (by default) reserves
a 1 megabyte region in the process private address space for the new thread's
stack. This means that there is an effective limit of approximately 2000
threads that can be created in each process (less depending on how much virtual
memory is being used for other reasons). This 1 megabyte default value can be
changed via a linker command line switch, or on a per-thread basis by
specifying a non-zero value for the dwStackSize parameter to CreateThread,
but there will still be a limit based on address space resources.
-
Page 503. Example 14-2 is mislabeled. It should be labeled Example
14-2. ExceptionFlow Program Output: No-Fault Scenario. The word "No-"
was missing from the label.
-
Page 515. The CSparseArray class doesn't compile
under Visual C++ 5.0. The declaration and inline implementation of the private ExceptionFilter
member function needs to be altered to compile under VC++ 5.0. Change the
signature of the function from int CSparseArray::ExceptionFitler( <arg
list> ) to int ExceptionFilter( <arg list>). In
other words, remove "CSparseArray::" from the signature.
There also appears to be a code generation error in VC++ 5.0. If the CSparseArray
class and associated test program SparseArrayTest are rebuilt in
release mode (with all the default optimizations turned on), the code
generated for the invocation of GetAt with an index
parameter of -1, as shown in Example 14-10 on page 521, does not include
the __try statement that protects access to the array. Since an
index of -1 is obviously out of bounds (on purpose), an exception is generated.
But since the use of the __try statement to guard access to the array
is ommitted by the code generator, the exception goes unhandled and results in
the unhandled exception dialog box being displayed. This behavior can be
reduced in separate test programs without using any code from the book, and the
problem has been submitted to Microsoft. One work around (aside from disabling
optimizations) is to change the GetAt function to use a temporary
local variable to cache the return value that is generated within the guarded
body and then use the return statement at the end of the GetAt
function instead of whithin the guarded body.
The SetAt function is apparently not affected by this bug. The key
combination seems to be the use of a return statement within the
guarded body of a __try/__except block inside of an inline function
(possibly due to the use of template parameters as well).
-
Apr_04_2005.zip. Latest version of the source
that includes the bug fix for the CMclLinkedListNode class described at the top
of this page.
-
Jun_23_2002.zip. Latest version of the source
that includes the bug fix for the CMclMailbox class described at the top of
this page.
-
Nov_07_2001.zip. Latest version of the source
that includes the bug fix for the CMclMailbox class described at the top of
this page.
-
June_02_2000.zip. Latest version of the source.
The Microsoft runtime libraries that correspond to this version are
here.
-
Rev03.zip. This zip
includes the complete source code found in the book and on the accompanying CD,
except for the Microsoft MFC and C-Runtime library DLLs. The runtime DLLs are
packaged separately in Runtimes.zip (available below). The following code has
been modified:
File |
Directory |
What's changed |
CMclKernel.h |
Mcl |
Object copy and assignment have been explicitly disallowed. |
CMclLinkedLists.h |
Mcl |
The race condition in CMclLinkedList has been corrected by using a semaphore
instead of an event to keep track of whether or not the list is empty. This fix
automatically affects the CMclQueue and CMclStack classes. |
DPQ1.cpp |
Chapter9 |
The CJobList::RemoveJob member function was updated to use the semaphore that
replaced the event in the CMclLinkedList class (which CJobList derives from). |
DPQ2.cpp |
Chapter9 |
The same fix applied to DPQ1.cpp was applied here. |
MclGlobal.h,.cpp |
Mcl |
New in Rev03.zip. Massaged CMclThrowError macro and
CMclInternalThrowError function to support Unicode builds of Mcl.lib. |
Mcl4Mfc.cpp |
Mcl4Mfc |
New in Rev03.zip. Changed constructors for
CMcl4MfcGUIThread and CMcl4MfcWorkerThread to use DuplicateHandle when making a
copy of the thread handle for use by the CMclKernel base class. Without this
alteration, both the CMclKernel and Mcl4Mfc thread classes held a copy of the
same handle variable. During cleanup, the thread class stops the thread and
closes the handle, which results in an invalid handle violation on NT in the
destructor for CMclKernel. |
*.dsp, *.dsw |
* |
New in Rev03.zip. All of the project files in this
drop of the source are VC 6.0 project files. |
-
Runtimes.zip.
Contains mfc42.dll, mfc42d.dll, msvcrt.dll, and msvcrtd.dll exactly as they
appear on the CD accompanying the book.
-
The switch
statement is not thread safe in Microsoft compilers. This does not mean you
cannot use a switch statement in multithreaded programs. What it means is that
you should not refer to any variables or data inside the switch expression
that might be accessed simultaneously by multiple threads. It is perfectly safe
to access local variables.
The issue with the switch statement is that it is not safe to do this:
DWORD
g_dwSomeGlobalVar; // A global variable
DWORD WINAPI ThreadProc( void *pvArg )
{
// This thread proc is called in the context of > 1
thread
// The following is NOT a thread-safe constuct:
switch( g_dwSomeGlobalVar )
{
case VALUE1: { ... } break;
case VALUE2: { ... } break;
:
}
}
The problems is that the switch statement is not necessarily a single, atomic
test statement. The compiler may generate code that reads the contents of
g_dwSomeGlobalVar multiple times to implement the switch statement. That
means another thead could update it's value while the switch statement is still
testing it.
Bottom line: don't switch on a variable that can be modified by more than 1
thread. If you want to switch on a global, just take a "snapshot" of it
by reading safely, caching it's current value in a local variable. Then
switch on the local variable:
DWORD WINAPI ThreadProc( void
*pvArg )
{
DWORD dwSomeVar = g_dwSomeGlobalVar;
switch( dwSomeVar )
{
case VALUE1: { ... } break;
case VALUE2: { ... } break;
:
}
}
If you're not just reading a machine word-sized variable, use a critical
section or mutex to safely read the variable's state. After that, other
threads may be modifying the global variable, but you're switch statement will
not risk seeing partially modified variables.
Copyright © 1998-2005 by Aaron Cohen and Mike
Woodring