Common Pitfalls in Multithreading: Navigating the Threaded Terrain

Common Pitfalls in Multithreading: Navigating the Threaded Terrain

October 18, 2023

1. The Treacherous Terrain of Multithreading

While multithreading offers a lot of advantages, it isn’t without its challenges. As developers, we need to be aware of these pitfalls to ensure our applications remain robust, efficient, and predictable.

In the last post, we explored the basics of multithreading in Python using asyncio and threading. In this post, we’ll dive into some of the most common pitfalls in multithreading, and how to avoid them, but this time using C and C++.


2. The Most Common Pitfalls

2.1 Race Connditions

The Great Cookie Jar Heist: 🫙🍪

Imagine two kids are trying to steal cookies from a jar at the same time. Without coordination, they might end up with more cookies than expected!

Let’s see this in action:

Here, we have two threads, kid1 and kid2, trying to steal cookies from the cookie jar. Each thread runs a loop to steal 100000 cookies. We expect the total number of cookies stolen to be 200000, but we might get a different result each time we run the program. Why? Because the threads are racing to increment the cookies variable. If kid1 reads the value of cookies as 0, and then kid2 reads it as 0, and then both increment it to 1, we’ve lost a cookie!

Now the question is, how do we prevent this? We need to ensure that only one thread can access the cookies variable at a time. We can do this by using a mutex (mutual exclusion lock):

What is a mutex?

A mutex is a variable that can be locked and unlocked. When a thread locks a mutex, it gains exclusive access to the variable. This prevents other threads from accessing the variable. Once the thread is done, it unlocks the mutex. This allows other threads to access the variable. If we use a mutex to protect the cookies variable, we can be sure that the total number of cookies stolen will always be 200000.

Here, we use the pthread_mutex_lock function to lock the mutex before incrementing the cookies variable. This ensures that only one thread can access the variable at a time.In other words, if kid1 locks the mutex, kid2 has to wait for kid1 to unlock the mutex before it can access the cookie jar. Once the thread is done, it unlocks the mutex using the pthread_mutex_unlock function. This allows other threads to access the variable. Now, we can be sure that the total number of cookies stolen will always be 200000.


2.2 Deadlocks

The Dueling Piano Players: 🎹

Two pianists want to play on two pianos. However, they booth need both pianos to play their duet. if they can’t agree on who gets to play which piano, they might just end up waiting forever! This is called a deadlock.

A deadlock is a situation where two or more threads are waiting for each other to finish, and none of them can proceed.

Let’s see this in action:

Here, we have two threads, player1 and player2, trying to play the pianos. Each thread locks both pianos before playing. This leads to a deadlock. player1 locks piano1 and then tries to lock piano2. However, player2 has already locked piano2. So, player1 waits for player2 to unlock piano2. Similarly, player2 waits for player1 to unlock piano1. This leads to a deadlock. Neither thread can proceed, and the program hangs.

Now the question is, how do we prevent this? We need to ensure that both threads lock the pianos in the same order. We can do this by using a mutex:

Here, we ensure that both threads lock piano1 first. This prevents a deadlock. Now, player1 locks piano1 and then piano2. player2 waits for player1 to unlock piano1. Once player1 unlocks piano1, player2 locks piano1 and then piano2. This allows both threads to proceed. Now, we can be sure that the program will not hang.


2.3 Starvation

The Hungry Birds: 🐓🦆🦜

A big bird keeps eating all the food, leaving the little birds hungry. This is starvation.

I’ll keep it simple,but in real world senarios, this can lead to performance issues. A more popular example of starvation is the Dining Philosophers Problem where a group of philosophers are seated around a table with a bowl of rice in the center. Each philosopher needs two forks to eat. However, there are only five forks on the table. So, if all philosophers pick up the fork on their left, no one can eat. This is called starvation.

Let’s see this in action:

Here, we have two threads, big_bird and little_bird, trying to eat from the food bowl. The big_bird thread keeps eating from the food bowl. The little_bird thread tries to eat from the food bowl, but it can’t because the big_bird thread is hogging the food bowl. This leads to starvation. The little_bird thread never gets to eat.

Now the question is, how do we prevent this? We need to ensure that the little_bird thread gets a chance to eat. We can solve this by using a mutex:

Here, we use a condition variable to ensure that the little_bird thread gets a chance to eat. The big_bird thread eats from the food bowl, and then signals the condition variable. This allows the little_bird thread to eat. The little_bird thread eats from the food bowl, and then signals the condition variable. This allows the big_bird thread to eat. This ensures that both threads get a chance to eat. Now, we can be sure that the little_bird thread will not starve.


2.4 Thread Thrashing

The Overbooked Dance Floor: 💃🪩🕺

It’s friday, you’re at the club and everyone wants to dance. However, the dance floor is too small to accommodate everyone. So, people keep bumping into each other, and no one can dance properly. That’s a problem! 🚨

But now seriously, this is called Thread Thrashing.

Let’s see this in action:

Here, we have 1000 dancers (threads) trying to dance. However, the dance floor is too small to accommodate all of them. So, the dancers keep bumping into each other, and no one can dance properly. This leads to thread thrashing. In other words, the threads keep switching between each other, and no thread can make any progress.

Now the question is, how do we prevent this? We need to ensure that the threads get a chance to dance. We can do this by using a semaphore.

What is a semaphore?

A semaphore is a variable that keeps track of the number of resources available. In this case, the semaphore keeps track of the number of dancers that can dance at a time. We can use a semaphore to ensure that only a certain number of threads can run at a time. This prevents thread thrashing.

Here, we use a semaphore, to ensure that only 10 dancers can dance at a time. The other dancers wait for space on the dance floor. This prevents thread thrashing. Now, we can be sure that the dancers will get a chance to dance.


2.5 Memory Leaks

The Forgetful Artist: 👨‍🎨🎨

Imagine an artist who keeps buying new paint but never uses it or throws any away. This is a memory leak. and security issues.

Let’s see this in action:

Here, we have an artist (thread) who keeps buying paint (allocating memory) but never uses it or throws any away. This leads to a memory leak. In other words, the artist keeps allocating memory but never frees it. This can lead to performance and security issues. For example, if the artist keeps allocating memory, the system might run out of memory and crash. From the security perspective, if the artist keeps allocating memory, a malicious user might be able to exploit this vulnerability to gain access to the system. This is called a buffer overflow attack which I might discuss in a future post. To prevent memory leaks, we need to ensure that the artist frees the paint after using it. Some programming languages like Python and Java have automatic garbage collection. However, in C, we need to manually free the memory.

Let’s see how we can do this:

Here, we ensure that the artist frees the paint after using it. This prevents memory leaks. Now, we can be sure that the artist will not run out of memory or any malicious user will not be able to exploit this vulnerability.


3. In Conclusion

In C++, you have two main options for working with threads: <thread> and <pthread.h>. <thread> is part of the C++ Standard Library, offering a user-friendly, object-oriented interface for creating and managing threads in a portable and standard way. On the other hand, <pthread.h> is a lower-level C library available on POSIX-compliant systems, providing more control but requiring manual resource management. <thread> is the preferred choice for most C++ projects, while <pthread.h>is used in POSIX-specific scenarios where fine-grained control is necessary or when you work with RT applications.

Multithreading is a powerful tool that can help us build robust, efficient, and predictable applications. However, it comes with its own set of challenges. As developers, we need to be cognizant of these pitfalls to ensure our applications remain robust, efficient, and predictable.

Last updated on