Multithreading in C
Introduction to Multithreading
Multithreading is a technique that allows a CPU to execute multiple threads concurrently, improving the performance and responsiveness of applications. In the context of the C programming language, multithreading is typically implemented using the POSIX Threads (pthread) library.
Setting Up
Before you start using pthreads, you need to include the pthread library in your program. This is done by including the <pthread.h>
header file and linking the pthread library during compilation.
Include the pthread header:
Compile with pthread library:
Creating Threads
To create a thread, you use the pthread_create
function. This function takes four arguments: a pointer to a pthread_t variable, thread attributes, the function to be executed, and the argument to be passed to the function.
Example of creating a thread:
#include <pthread.h> #include <stdio.h> #include <stdlib.h> void* print_message(void* message) { printf("%s\n", (char*)message); return NULL; } int main() { pthread_t thread; const char* message = "Hello, Thread!"; int result = pthread_create(&thread, NULL, print_message, (void*)message); if (result != 0) { printf("Error creating thread: %d\n", result); return 1; } pthread_join(thread, NULL); return 0; }
Joining Threads
After creating a thread, the main thread can wait for it to finish by using the pthread_join
function. This function takes two arguments: the thread to wait for and a pointer to store the thread's return value.
Example of joining a thread:
#include <pthread.h> #include <stdio.h> void* thread_function(void* arg) { printf("Thread is running...\n"); return NULL; } int main() { pthread_t thread; pthread_create(&thread, NULL, thread_function, NULL); pthread_join(thread, NULL); printf("Thread has finished.\n"); return 0; }
Thread Synchronization
In a multithreaded environment, it's crucial to manage access to shared resources to avoid race conditions. The pthread library provides several synchronization mechanisms, including mutexes.
Using Mutexes
A mutex (mutual exclusion) is a synchronization primitive used to protect shared data from being simultaneously accessed by multiple threads. You initialize a mutex using pthread_mutex_init
, lock it using pthread_mutex_lock
, and unlock it using pthread_mutex_unlock
.
Example of using a mutex:
#include <pthread.h> #include <stdio.h> pthread_mutex_t mutex; int counter = 0; void* increment_counter(void* arg) { pthread_mutex_lock(&mutex); counter++; printf("Counter: %d\n", counter); pthread_mutex_unlock(&mutex); return NULL; } int main() { pthread_t thread1, thread2; pthread_mutex_init(&mutex, NULL); pthread_create(&thread1, NULL, increment_counter, NULL); pthread_create(&thread2, NULL, increment_counter, NULL); pthread_join(thread1, NULL); pthread_join(thread2, NULL); pthread_mutex_destroy(&mutex); return 0; }
Condition Variables
Condition variables are used to block a thread until a particular condition is met. They are always used with a mutex to avoid race conditions. You initialize a condition variable using pthread_cond_init
, wait on it using pthread_cond_wait
, and signal it using pthread_cond_signal
or pthread_cond_broadcast
.
Example of using condition variables:
#include <pthread.h> #include <stdio.h> pthread_mutex_t mutex; pthread_cond_t cond; int ready = 0; void* wait_for_signal(void* arg) { pthread_mutex_lock(&mutex); while (!ready) { pthread_cond_wait(&cond, &mutex); } printf("Signal received!\n"); pthread_mutex_unlock(&mutex); return NULL; } void* send_signal(void* arg) { pthread_mutex_lock(&mutex); ready = 1; pthread_cond_signal(&cond); pthread_mutex_unlock(&mutex); return NULL; } int main() { pthread_t thread1, thread2; pthread_mutex_init(&mutex, NULL); pthread_cond_init(&cond, NULL); pthread_create(&thread1, NULL, wait_for_signal, NULL); pthread_create(&thread2, NULL, send_signal, NULL); pthread_join(thread1, NULL); pthread_join(thread2, NULL); pthread_mutex_destroy(&mutex); pthread_cond_destroy(&cond); return 0; }
Advanced Topics
In this section, we'll cover some advanced topics in multithreading, including thread pools and barriers.
Thread Pools
A thread pool is a collection of pre-initialized threads that can be reused to perform multiple tasks. This reduces the overhead of creating and destroying threads frequently.
Implementing a simple thread pool is beyond the scope of this tutorial, but you can explore libraries like libuv
or Threading Building Blocks (TBB)
for more advanced usage.
Barriers
A barrier is a synchronization primitive that allows multiple threads to wait until all threads have reached a certain point in the code. You initialize a barrier using pthread_barrier_init
and wait on it using pthread_barrier_wait
.
Example of using a barrier:
#include <pthread.h> #include <stdio.h> pthread_barrier_t barrier; void* thread_task(void* arg) { printf("Thread %ld reached the barrier.\n", (long)arg); pthread_barrier_wait(&barrier); printf("Thread %ld passed the barrier.\n", (long)arg); return NULL; } int main() { pthread_t threads[3]; pthread_barrier_init(&barrier, NULL, 3); for (long i = 0; i < 3; i++) { pthread_create(&threads[i], NULL, thread_task, (void*)i); } for (int i = 0; i < 3; i++) { pthread_join(threads[i], NULL); } pthread_barrier_destroy(&barrier); return 0; }
Conclusion
Multithreading is a powerful technique that can significantly improve the performance and responsiveness of applications. However, it also introduces complexity, especially with regard to synchronization and resource sharing. By mastering the basics of pthreads, you can begin to leverage the full power of multithreading in your C programs.