Published on 2024-12-31
Could be Interesting!I don't know why they are called threads but Multi-threading distributes tasks across threads, preventing bottlenecks and idle CPU time.
While multiprocessing fulfills the job of concurrency, it can be cumbersome to share information across processes. Threading is a way to achieve the parallel processing within a single process and because threads share the same memory space (globals, heap, pass by reference, etc.), communication between threads is faster than between processes.
thread myThread(myFunc, arg1, arg2);
thread.join() // thread joining back the main process
One issue with the concurency is that if properly not handled, a race condition can occur when multiple threads or processes access shared resources (e.g, variables, files or memory) simultaneously, and the final outcomes depends on the timing of their execution. This can lead to unpredictable behaviors. One of the ways to prevent race condition is the use of mutex, short for mutal exclusion. It is a type of variable, which a thread will lock it before accessing and unlocking it after the operation is complete. While the mutex is locked, other threads trying to access the resource will have to wait. The code block between the lock and unlock actions is called the critical section. A very simple code below:
#include <iostream>
#include <thread>
#include <mutex>
std::mutex mtx; // Declare a mutex
int balance = 100; // Shared resource
void withdraw(int amount) {
mtx.lock(); // Lock the critical section
if (balance >= amount) {
std::cout << "Withdrawing " << amount << " from balance " << balance << "\n";
balance -= amount;
std::cout << "New balance: " << balance << "\n";
} else {
std::cout << "Insufficient funds for withdrawal of " << amount << "\n";
}
mtx.unlock(); // Unlock the critical section
}
int main() {
// Create threads simulating two withdrawals
std::thread t1(withdraw, 50);
std::thread t2(withdraw, 70);
// Wait for threads to complete
t1.join();
t2.join();
std::cout << "Final balance: " << balance << "\n";
return 0;
}
Note: A deadlock occurs in multithreading when two or more threads are waiting for resources held by each other, creating a situation where none of the threads can proceed.
When a thread (let's say thread-1) locks the critical section, the waiting threads (let's say thread-2 and thread-3) have to continuously monitor whether the lock has been released and this continous monitoring or 'busy waiting' consumes the resources. Wouldn't it be neat if the thread-1 can just tell the other waiting threads it has released the lock? That is exactly what a conditional variable does. In addition to notifying other threads to wake up, it can also check if a certain condition is fulfilled. Here's an improved version of cash withdrawal function with conditional variable:
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
std::mutex mtx; // Mutex for synchronization
std::condition_variable cv; // Condition variable for signaling
int balance = 100; // Shared resource
void withdraw(int amount) {
std::unique_lock<std::mutex> lock(mtx); // Lock the critical section
if (balance >= amount) {
std::cout << "Withdrawing " << amount << " from balance " << balance << "\n";
balance -= amount;
std::cout << "New balance: " << balance << "\n";
} else {
std::cout << "Insufficient funds for withdrawal of " << amount << "\n";
}
cv.notify_all(); // Notify all other threads after the critical section
}
int main() {
// Create threads simulating two withdrawals
std::thread t1(withdraw, 50);
std::thread t2(withdraw, 70);
// Wait for threads to complete
t1.join();
t2.join();
std::cout << "Final balance: " << balance << "\n";
return 0;
}
In addition to conditional variable, there is also a counter semaphore which limits the number of threads that can access a shared resource simultaneously. The best way to understand the concurrency concept is to implement the lock mechanism with mutuex, conditional variables and semaphore. :)