A Brief Guide to Writing Thread Safe Code
This technical paper was written by David Clarke of Dragon
Thoughts
Ltd
and is Copyright David Clarke © 1999
This document is a set of guidelines for avoiding some of the major problems caused
by multi-threading. It has a primary focus on the Windows 32 bit platforms but has
relevance to any operating system.
The purpose of this document is to provide some guidelines to ensure that the
most common errors encountered in writing multi-threaded code in C++ are easily avoided.
It does not seek to be a complete reference for multi-threading.
Target Audience
The intended audience is anyone writing multi-threaded code.
General Threading Issues
When writing any multi threaded software on Windows, it is important to be aware
of the following:
- That all the threads reside in the same memory space as the process which create
them.
- A thread can be interrupted during any operation, including in the middle of
an assignment statement or construction of an object.
- There are specific Synchronisation mechanisms provided by the operating system,
that must be used to ensure that threads cannot interfere with each others
operations.
- If a thread crashes, it may lock DLLs in memory or even bring down the entire
application.
Things that Must Never be Done
There is a range of techniques that should never be used in multi-threading unless
specifically controlled through the use explicit synchronisation objects, which are
specifically designed to handle thread interaction issues.
- Use global variables.
- Use Static member variables in classes.
- Throw an exception in one thread and expect the threads creator to catch
it.
- Call TerminateThread() on Windows.
- Build busy loops, by getting threads to repeatedly poll for information when
they have nothing else to do.
Techniques to be Avoided
There are techniques that are potentially dangerous in a threading environment, and
should be avoided unless there is overwhelming reason to perform them.
- Altering the priority of a thread significantly from others in the same process.
- Using shared memory, in particular remember, never to put C++ classes with
virtual functions or internal pointers into shared memory
- C style casts
- Pointers to functions: consider whether inheritance with virtual methods would
be better.
Techniques that are Safe or Good
- Keep information sharing between threads to a minimum.
- Using of the NT kernel based threading objects and their related classes (e.g.
CMutex) to communicate with thread.
- Use of named or unnamed pipes for inter process communication.
- Procedures, functions and methods should be kept short and well defined, particularly
if access to them may be controlled as critical sections or mutexes.
- For constructors of classes, try to perform the initialisation before the statement
parts of the construction, then if the construction crashes, the exception handler
has a good chance of cleaning up the instance.
- Methods that really should not be modifying the state of an object should be
declared as "const".
- If an object has a member variable that is a pointer to an object and the constructor
is always going to call "new" to create an instance of the object then
the object should be a direct member of the class. This will improve speed and
reliability of construction and destruction and reduce memory leakage errors.
- If a pointer to an object is to be stored, consider whether a reference to
the object might be more appropriate.
Things that must be done
- All code must be re-entrant.
- All code must be linked with multithreaded runtime libraries.
- If a flag must be set for multiple threads to recognise, (e.g. signifying an
exit status) then an event object is the correct thing to use.
- Wait for kernel based threading objects using "Wait " calls rather
than sleeping and polling. Remember, most of the "Wait " calls can
be given a timeout option if appropriate.
- Declare variables and parameters that could be updated by another thread as
"volatile" to prevent compiler optimisations generating problems.
- Destructors must be "virtual".
- Be sure that called libraries are thread safe (e.g. DAO is not thread safe,
and so should not be used except in the primary thread of an application)
Synchronisation Mechanisms in Windows, using MFC
In general the Kernel objects will still be cleared up and stop blocking other threads
when the thread currently using the object crashes. More detailed discussions can
be found in see chapter 4, "Synchronisation", of
"
Multithreading Applications in Win32"
by Jim Beveridge and Robert Wiener.
The MFC classes that deal with synchronisation should be used. The six multithreaded
classes provided with MFC fall into two categories: synchronisation objects (CSyncObject,
CSemaphore, CMutex, CCriticalSection, and CEvent) and synchronisation access objects
(CMultiLock and CSingleLock).
Critical Section
A critical section is used to enforce mutual exclusion between thread within a single
process. A critical section:
- Is a local object, not a kernel object
- Is fast
- Cannot be waited on more that one at a time
- Cannot determine if it was abandoned by a thread
Mutex
A mutex is a kernel object that will enforce mutual exclusion between threads even
if they are in different processes. A mutex:
- Is a Kernel object.
- Generates an "abandoned" error if the owning thread terminates
- Can be used in Wait () calls.
- Is named and can be opened across processes.
- Can only be released by the thread that owns it.
Semaphore
The semaphore is used to keep track of a limited resource. A semaphore:
- Is a Kernel object.
- Has no owner.
- Is named and can be opened across processes.
- Can be released by any thread
Event Object
Event objects are used for overlapped I/O and for some custom designed objects. An
event object:
- Is a Kernel object
- Is completely under program control.
- Is good for designing new synchronisation objects.
- Does not queue wake-up requests.
- Is named, and can be opened across processes.
Interlocked variable
The Interlocked..() calls are only synchronisation mechanism if they are used for
a spin-lock, which is a busy loop that is expected to be running for such as short
time that it has minimal overhead. The kernel uses these occasionally. Other than
that, interlock variables are primarily useful for reference counting. They:
- Allow basic operations on 4-byte values without having to use a critical section
or mutex.
- Work even on multi-processor systems.
Summary
Keep information sharing between threads to a minimum.
Information that must be shared should only be shared through the specifically
thread safe entities.
If any words or phrases in this document are unfamiliar to you, and you are developing
multi-threaded code, find out about them and their implications.
This technical paper was written by David Clarke of Dragon
Thoughts
Ltd
and is Copyright David Clarke © 1999