4 minutes
An Introduction to Mutexes and Multithreading in C
Title: An Introduction to Mutexes and Multithreading in C
In the world of programming, threads are a crucial concept that allows for concurrent execution within a single process. This concurrent execution is made possible through multithreading, a widespread technique in modern programming. However, when multiple threads access a shared resource, synchronization problems can arise. In this blog post, we’ll explore how to use Mutexes in C for thread synchronization and delve into the concept of multithreading.
Understanding Thread Synchronization Problems
Consider a simple C program where two threads (jobs) are created. Each thread increments a counter and logs a message when it starts and finishes its job. However, if we observe the output, we might notice some unexpected behavior.
For instance, you might see that the log ‘Job 2 finished’ appears twice, while ‘Job 1 finished’ is not logged at all. At first glance, the code might seem fine, but upon closer examination, you’ll discover that the problem lies with the shared resource ‘counter’ being accessed by both threads without synchronization. The lack of synchronization leads to the second thread using the ‘counter’ variable while the first thread was still using or about to use it, hence leading to synchronization problems【5†source】.
The Role of Mutexes
Mutexes are the solution to the problem of thread synchronization. A Mutex (short for “mutual exclusion”) is essentially a lock that a thread sets before using a shared resource and releases after using it. When the lock is set, no other thread can access the locked region of code. This mechanism ensures synchronized access to shared resources in the code
How Mutexes Work Internally
Here’s a step-by-step breakdown of how mutexes work:
- Suppose one thread has locked a region of code using a mutex and is executing that piece of code.
- If the scheduler decides to perform a context switch, all the other threads ready to execute the same region are unblocked.
- One of these threads makes it to execution, but if it tries to execute the same region of code that’s already locked, it will go to sleep.
- Context switches will continue, but no thread will execute the locked region of code until the mutex lock is released.
- The mutex lock is only released by the thread that locked it in the first place.
- This mechanism ensures that once a thread has locked a piece of code, no other thread can execute the same region until it is unlocked by the thread that locked it【6†source】.
Initializing and Locking Mutexes
In C, a mutex is initialized and then locked by calling the following two functions:
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
int pthread_mutex_lock(pthread_mutex_t *mutex);
The first function initializes a mutex, and through the second function, any critical region in the code can be locked.
To release the lock and destroy the mutex, we use the following functions:
int pthread_mutex_unlock(pthread_mutex_t *mutex);
int pthread_mutex_destroy(pthread_mutex_t *mutex);
The first function releases the lock, and the second function destroys the lock so that it can’t be used in the future【6†source】.
A Practical Example of Mutexes in C
Let’s look at an example where mutexes are used for thread synchronization. The ‘counter’ variable is shared between two threads, and a mutex is used to lock this variable:
#include<stdio.h>
#include<string.h>
#include<pthread.h>
#include<stdlib.h>
#include<unistd.h>
pthread_t tid[2];
int counter;
pthread_mutex_t lock;
void* doSomeThing(void *arg)
{
pthread_mutex_lock(&lock);
unsigned long i = 0;
counter += 1;
printf("\n Job %d started\n", counter);
for(i=0; i<(0xFFFFFFFF);i++);
printf("\n Job %d finished\n", counter);
pthread_mutex_unlock(&lock);
return NULL;
}
int main(void)
{
int i = 0;
int err;
if (pthread_mutex_init(&lock, NULL) != 0)
{
printf("\n mutex init failed\n");
return 1;
}
while(i < 2)
{
err = pthread_create(&(tid[i]), NULL, &doSomeThing, NULL);
if (err != 0)
printf("\ncan't create thread :[%s]", strerror(err));
i++;
}
pthread_join(tid[0], NULL);
pthread_join(tid[1], NULL);
pthread_mutex_destroy(&lock);
return 0;
}
In this code:
- A mutex is initialized at the start of the
main
function. - The same mutex is locked in the
doSomeThing()
function while using the shared resource ‘counter’. - At the end of the
doSomeThing()
function, the same mutex is unlocked. - Finally, at the end of the
main
function, when both threads are finished, the mutex is destroyed【7†source】.
With this change, the output now correctly logs the start and finish of both jobs:
Job 1 started
Job 1 finished
Job 2 started
Job 2 finished
So we see that by using a mutex, we have achieved thread synchronization.
Conclusion
In multithreaded programming, thread synchronization is essential for ensuring that shared resources are accessed and modified in a coordinated manner. Mutexes in C offer a reliable way to achieve this synchronization, by providing a locking mechanism that restricts access to a shared resource to one thread at a time. By understanding and implementing these concepts, you can write more reliable and bug-free multithreaded programs. Happy coding!
I hope you find this information useful. If you have any more questions, feel free to ask!