Recently when reviewing some piece of code, which was going to be used in a multithreaded environment, I came to the conclusion that writing thread-safe code is twice as difficult as regular single-threaded code.
Concurrency adds another dimension to programs. A function in a multi-threaded program must be aware that other pieces of code will be executing at the same time. What happens when two threads call this function simultaneously? What happens if other functions start changing the data this function uses?
Not only code has to be written making sure that nobody messes it up during execution, but the lifetime of resources must also be considered. The resources used by a thread must be allocated and freed even more diligently than in single-threaded programs. Threads are also resources, while it’s easy to spawn them, stopping them must also be planned.
Here are a few tips for those new to the world of multiple threads of execution. There are exceptions to every “rule” described below, so always stay alert!
- Use mutexes to guard resources which can be modified simultaneously by multiple threads.
- Reading is a thread-safe operation, unless someone can modify the data you’re reading. Data which is initialized once before any threads are created or const data does not have to be guarded – unless multiple threads can attempt to initialize it.
- Use as few mutexes as possible to reduce the risk of deadlocks.
- When using multiple mutexes to lock multiple resources, always lock mutexes in the same order and unlock them in reverse order to avoid deadlocks.
- Prefer lock-free constructs. They usually rely on atomic operations. Creating a good lock-free construct can be quite tricky. If multiple atomic operations are needed, maybe it’s time to use a mutex – multiple atomic operations can lead to race conditions.
- A good design pattern applicable to many situations is a queue with worker threads. Such queue can be designed to be fed by either one or many threads, which will keep adding elements to its tail. Worker threads will keep retrieving items from the head of the queue. In some situations a lock-free queue is useful, in other situations it’s good to have a semaphore to indicate to worker threads if something is in the queue and a mutex to guard the queue’s guts. Such queue is quite easy to write and reasonably safe.
- Alternatively you can think of such queue as of a messaging mechanism, where threads can interchange information using messages. Software which relies on message passing instead of sharing data between threads statistically has a lower chance of defects.
- Another relatively safe construct are barriers. In a model with barriers you create multiple seemingly identical threads, which occasionally wait on a barrier to synchronize with each other. All threads in a group must stop on a barrier, only then can they continue further execution. The programming model based on barriers can be found in CUDA or OpenCL and is relatively safe, although deadlocks may occur if threads have a way to avoiding barriers.
- Group resources into classes and guard them with a single mutex. Have all the public functions of such classes lock the mutex.
- In general highly cohesive modules are easier to maintain in multi-threaded environments.
- For every function make sure that it is safe to call it simultaneously from multiple threads.
- For every class make sure that it is safe to call any number and combination of the class’ function simultaneously from multiple threads.
- Make all class’ member variables private. This is generally encouraged in C++, but it’s even more important for thread safety.
- Make sure that functions you call are thread-safe.
- Avoid global variables. You can get away with them in single-threaded programs, but they can and will mess things up severely in multi-threaded code.
- Avoid static variables. They are just a form of globals and will also lead to problems.
- Avoid nested mutexes. Some platforms allow nested mutex locking, such as Windows’ critical section or a recursive POSIX mutex. The problem is that when you determine that you need a recursive mutex it is a symptom of a bad multi-threaded design. There are exceptions to this, as always, but usually the code will sooner or later get out of hand and you will spend a lot of time debugging spurious failures. Once you need a recursive mutex it indicates that there is no one, clean way to enter a function or section of code and users of the code may enter into situations which you failed to predict.
- Avoid passing function arguments and local variables to other threads by pointer or reference. This way you can rely on them as always being thread-safe.
- When passing data between threads, design well when that data will be created and destroyed. Make sure it doesn’t leak. Make sure it isn’t accessed after it’s destroyed.
- When writing C or C++ code, avoid the volatile keyword, unless you use it for hardware resources and you know exactly what you are doing. The volatile keyword has nothing to do with thread safety.
If you are writing single-threaded code, keep in mind that some day you or somebody else may need to use it in a multi-threaded environment. Therefore most of the hints above apply to some extent to single-threaded programs as well!