Common Multithreading Mistakes

Akhil Robertson Cutinha
The Startup
Published in
6 min readJan 9, 2021

Threading is one of the most complicated things to get right in programming, especially in C++. Bugs are frequently encountered in multithreaded programs more frequently than we would like. Sometimes these bugs tread into production software causing live systems to be patched causing an additional expense.

In this article, I will be highlighting some of the common mistakes I found in various projects using the Coderrect tool.

  1. Not protecting shared data or shared resources

In a program running multiple threads, access to a resource is usually competitive. The threads compete with each other in accessing the shared variable. This often results in ambiguous results from the program. Usually, this can be detected by running the program multiple times because the program would not give a uniform output. This can be prevented by using some mechanism that only allows ONE thread to act on a shared resource at a time.

#include <iostream>
#include <thread>
using namespace std;void greet(string location) {
cout << "Thread " << this_thread::get_id() << " says " <<
location<< endl;
}
int main() {
thread t1(greet, "Howdy from Texas");
thread t2(greet, "Hello from California");
thread t3(greet, "Hi from New York");
greet("Hey from Washington"); thread t4(greet, "Wahat's up from Illinois");
thread t5(greet, "Ola from Florida");
t1.join();
t2.join();
t3.join();
t4.join();
t5.join();
return 0;
}

Upon running the program we get the following output;

This is because the five threads get the std::cout resource in a random fashion. The solution to this problem is to protect access to std::cout resource using a locking system like std::mutex. Just change the greet() to acquire a mutex before using std::cout and release it after it’s done.

#include<mutex>std::mutex mu;void greet(string location) {
mu.lock();
cout << "Thread " << this_thread::get_id() << " says " <<
location<< endl;
mu.unlock;
}

Example: https://github.com/arbruijn/olmod

The project contains data races as listed below using the Coderrect Static debugger.

Obtained using Coderrect

2. Deadlock Condition — Wrong Implementation of Locking Mechanism

A common mistake is caused while trying to prevent the previous mistake. The random access to shared variables is limited using a locking system. However, incorrect implementation of a locking system may cause a condition that is known as a deadlock. In this scenario, the threads prevent each other from executing their commands.

In the above example, we used the lock() and unlock() syntax on mutex. Now, suppose we forget to unlock() the mutex. This causes the std::cout resource to be locked and restricted to just one thread preventing the other threads from accessing the resource indefinitely.

void greet(string location) {
mu.lock();
cout << "Thread " << this_thread::get_id() << " says " <<
location<< endl;
//mu.unlock;
}

Upon running the program we get the following output;

The code hanged on the console and did not terminate

Sometimes these simple syntax errors may be hard to find and rectify due to the size and complexity of a multithreaded program. Therefore, it is always suggested to use std::lock_guard which uses RAII style to manage the duration of a mutex lock. When the lock_guard object goes out of scope, the lock_guard object is destroyed which releases the mutex.

void greet(string location) {

//lock_guard object is and mutex is aquired
std::lock_guard<std::mutex> lock (mu);
cout << "Thread " << this_thread::get_id() << " says " <<
location<< endl;
} //lock_guard object is destroyed and mutex mu is released

3. Deadlock Condition — Misordering of Locks

The deadlock condition can be caused due to another crucial error: misplacing the lock order. Consider two threads;

In some situations, what’s going to happen is that when Thread 1 tries to acquire Lock B, it gets blocked because Thread 2 is already holding lock B. According to Thread 2, it is blocked on acquiring lock A but cannot do so because Thread 1 is holding lock A. Thread 1 cannot release lock A unless it has acquired lock B and so on. Thus the program hangs and does not execute further.

Fortunately, this problem can be sorted using either of the 2 ways;

  • Acquire multiple locks together if they have to be acquired
std::scoped_lock lock{muA, muB};
  • You can use a timed mutex where you can mandate that a lock be released after a timeout if it’s not already available.

4. Lengthy Critical Sections

When one thread is executing inside the critical section(the section that is being called by multiple threads), all other threads trying to enter the critical section are blocked. So we should keep the instructions inside a critical section as small as possible. To illustrate, here’s a bad piece of critical section code.

void greet(string location) {

//lock_guard object is and mutex is aquired
std::lock_guard<std::mutex> lock (mu);
Read_five_thousand_entries(); cout << "Thread " << this_thread::get_id() << " says " <<
location<< endl;
} //lock_guard object is destroyed and mutex mu is released

The function Read_five_thousand_entries() is a read-only process. This function works the same even outside the lock. Let’s assume it takes 10 seconds to read the entries from a database. All the other threads are blocked for that time period unnecessarily. These types of errors cause potentially big problems when the scale of the program increases considerably.

The correct way is to just keep the std::cout under the section subject to the locking mechanism.

void greet(string location) {    Read_five_thousand_entries();    //lock_guard object is and mutex is aquired
std::lock_guard<std::mutex> lock (mu);
cout << "Thread " << this_thread::get_id() << " says " <<
location<< endl;
} //lock_guard object is destroyed and mutex mu is released

5. Failure to use a Thread Pool

One of the key benefits of multithreading is the ability to perform routine tasks like logging data, updating data to the cloud, and sending data across servers while waiting for the user to finish typing. Creating and deleting threads are expensive in terms of CPU time. Imagine trying to create a thread while a CPU intensive task like rendering graphics or calculating game physics is going on. A technique often adopted to help improve this situation is to create a pool of preallocated threads that can handle the routine tasks.

Also, by using this technique, all the gory details of thread lifecycle management are taken care of. This means less code and fewer bugs!

Libraries that implement thread pools are:

6. Terminating an Application before termination of Background Threads

A thread must be joined using std::join() or be detached(making it unjoinable) before an application is closed. If the main program is terminated without completing this fundamental step, the program crashes. This happens because, when the program is terminated without joining a thread, the thread goes out of scope and the thread destructor is called. The destructor first checks whether the thread is joinable or is detached. As our thread is out of scope, it causes the program to fail.

This error can be fixed either by joining the thread or detaching it using detach().

using namespace std;<thread_name>.join(); //Syntax for Joining a thread<thread_name>.detach(); //Syntax for detaching a thread

7. Trying to Join a Daemon(detached) Thread

Another common problem seen in multithreaded programs is ‘trying to join a detached thread’. Sometimes a thread is detached from the main program on purpose to attain certain results. However, a few hundred lines later, code is written to join the same thread back to the main program. This is a fatal error because detaching a thread from the main function makes it ‘unjoinable’. Therefore, attempting to join it again causes the program to crash. An important note to note here is that this mistake does not cause a compiler error, rather it crashes the program itself.

A simple way to prevent this mistake from happening is to always check whether a thread is joinable using joinable() before joining it to the main function.

if(<thread_name>.joinable()) //cheaking if the thread is joinable
<thread_name>.join();

Conclusion

These were some of the common causes that led to data races and deadlock conditions in multithreaded programs. This was a fun project to learn the various causes behind an error. As an interesting fact, the majority of these errors are caused due to improper implementation of programming concepts. This can be assumed to be due to a lack of understanding of the background processes of a function. I hope this article has helped you gain some deep perspective into the topic and will help you in the future to stay clear of these pitfalls.

References

Haldar, D. (2017, September 20). Top 20 C++ multithreading mistakes and how to avoid them. Retrieved October 01, 2020, from https://urldefense.com/v3/__https://www.acodersjourney.com/top-20-cplusplus-multithreading-mistakes/__;!!KwNVnqRv!VpaT07CP-gR_UG1y6NY6NZsguissuTJGlL4dd7scosML5c30oqs-dm0l2xj9QtC3mdI$

Project List:

AI State full task, Apollo, Emscripten, Envoy, Multithreaded Vowel Counter, Networkit, Olmod, OpenPilot, OpenTTD, Pennant, RayTracer, Sanmil, Sleeping Barber Problem, Spotify — Echoprint, timer, WebAPI, Xenia.

--

--