Thread programming, often referred to as multi-threading, is a method used to achieve parallel execution in software applications by utilizing multiple threads of execution. A thread is the smallest unit of a CPU's execution and a way to split a program into multiple tasks that can run concurrently, thus improving performance, especially on multi-core processors.
Thread:
Concurrency vs. Parallelism:
Thread Creation:
Thread Synchronization:
Thread Communication:
Thread Creation in C++/C:
In C++ and C, threads are usually created using libraries such as POSIX Threads (pthreads) or C++11 thread support.
POSIX Threads (pthreads):
#include <pthread.h>
#include <stdio.h>
// Function to be executed by each thread
void* print_hello(void* arg) {
printf("Hello from thread %d\n", *((int*)arg));
return NULL;
}
int main() {
pthread_t threads[5];
int thread_args[5];
// Create 5 threads
for (int i = 0; i < 5; i++) {
thread_args[i] = i;
pthread_create(&threads[i], NULL, print_hello, (void*)&thread_args[i]);
}
// Join the threads to wait for their completion
for (int i = 0; i < 5; i++) {
pthread_join(threads[i], NULL);
}
return 0;
}
In this example:
pthread_create creates a new thread.pthread_join waits for the threads to finish their execution.C++11 <thread> Library:
<thread> library, which provides a more convenient interface for creating and managing threads.#include <iostream>
#include <thread>
// Function to be executed by the thread
void print_hello(int thread_id) {
std::cout << "Hello from thread " << thread_id << std::endl;
}
int main() {
std::thread threads[5];
// Create 5 threads
for (int i = 0; i < 5; i++) {
threads[i] = std::thread(print_hello, i);
}
// Join the threads
for (int i = 0; i < 5; i++) {
threads[i].join();
}
return 0;
}
Here, std::thread is used to create threads, and join() ensures that the main program waits for the threads to complete.
Thread Synchronization: Thread synchronization is required when threads share resources or data. The key challenge in synchronization is ensuring that threads do not conflict when accessing shared resources, leading to data races or inconsistencies.
Common synchronization mechanisms include:
Mutex (Mutual Exclusion): A mutex is a lock that ensures only one thread can access a critical section of code at a time. Other threads that attempt to access the locked section will block until the mutex is unlocked.
Example of using a mutex in C++:
#include <iostream>
#include <thread>
#include <mutex>
std::mutex mtx;
void print_hello(int thread_id) {
mtx.lock(); // Lock the mutex
std::cout << "Hello from thread " << thread_id << std::endl;
mtx.unlock(); // Unlock the mutex
}
int main() {
std::thread threads[5];
for (int i = 0; i < 5; i++) {
threads[i] = std::thread(print_hello, i);
}
for (int i = 0; i < 5; i++) {
threads[i].join();
}
return 0;
}
Condition Variables: Condition variables allow threads to wait for certain conditions to be met before continuing. They are used to coordinate the activities of multiple threads.
Example of using condition variables in C++:
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
std::mutex mtx;
std::condition_variable cv;
bool ready = false;
void print_hello(int thread_id) {
std::unique_lock<std::mutex> lock(mtx);
while (!ready) cv.wait(lock); // Wait until 'ready' is true
std::cout << "Hello from thread " << thread_id << std::endl;
}
void go() {
std::unique_lock<std::mutex> lock(mtx);
ready = true;
cv.notify_all(); // Notify all waiting threads
}
int main() {
std::thread threads[5];
for (int i = 0; i < 5; i++) {
threads[i] = std::thread(print_hello, i);
}
std::cout << "Preparing to start threads..." << std::endl;
go(); // Set ready to true and notify threads
for (int i = 0; i < 5; i++) {
threads[i].join();
}
return 0;
}
Thread Safety: Thread safety refers to the concept of ensuring that shared data is accessed in a way that prevents unexpected or inconsistent behavior. Key techniques for ensuring thread safety include:
Thread-safe functions or objects ensure that data is protected against simultaneous access by multiple threads, preventing race conditions.
Thread Pooling: A thread pool is a collection of pre-allocated threads used to execute tasks concurrently. This is more efficient than creating and destroying threads for each task, especially in scenarios where many short-lived tasks need to be executed. Thread pools are particularly useful in server applications where requests need to be processed concurrently.
In C++, you can use libraries such as std::async or external libraries like Intel Threading Building Blocks (TBB) or Boost.Thread to implement thread pools.
Race Conditions: A race condition occurs when two or more threads try to modify shared data simultaneously without proper synchronization. This leads to inconsistent or incorrect results.
Deadlocks: A deadlock occurs when two or more threads are blocked indefinitely because they are waiting for each other to release resources. This typically happens in systems with multiple resources where threads lock resources in different orders.
Thread Scheduling: Threads are scheduled by the operating system, and their execution can be preempted by other threads. This means that thread execution can be unpredictable, leading to challenges in debugging and testing.
Memory Issues: Managing memory in multi-threaded environments can be tricky. Threads often share memory space, and improper synchronization can lead to issues like data corruption or crashes.
Thread programming is an essential technique for creating parallel and high-performance applications. It allows programs to perform multiple operations simultaneously, taking advantage of multi-core processors and improving responsiveness. While powerful, thread programming comes with challenges like synchronization, race conditions, and deadlocks. By using appropriate synchronization mechanisms, ensuring thread safety, and leveraging thread pools, developers can create efficient and robust multi-threaded applications.
Open this section to load past papers