top of page

Synchronization Hardware and Posix Threads: Optimizing Multithreaded Applications



Synchronization hardware plays a crucial role in the proper functioning of modern operating systems. It ensures that multiple processes or threads can access shared resources in a safe and efficient manner. In this tutorial, we will discuss the basics of synchronization hardware in OS and some of the commonly used synchronization primitives.


📕 What is Synchronization Hardware?


Synchronization hardware refers to the various hardware mechanisms that an operating system uses to coordinate the access of multiple threads or processes to shared resources. Shared resources include anything that multiple threads or processes can access, such as memory, files, and input/output devices. Synchronization hardware is essential because multiple threads or processes can interfere with each other when they access shared resources simultaneously.


📕 Synchronization Primitives


Synchronization primitives are a set of instructions or functions that an operating system provides to enable synchronization between threads or processes. Here are some of the commonly used synchronization primitives:


✔ Locks


Locks are the most commonly used synchronization primitive. A lock is a mechanism that allows only one thread or process to access a shared resource at a time. When a thread or process acquires a lock, it gains exclusive access to the resource until it releases the lock. Locks come in different types, such as binary locks, spin locks, and reader-writer locks.

Here is an example of how locks work in C:


#include <pthread.h>

pthread_mutex_t lock;

void* thread_func(void* arg) {
    // Acquire lock
    pthread_mutex_lock(&lock);

    // Access shared resource// Release lock
pthread_mutex_unlock(&lock);
}

int main() {
    // Initialize lock
pthread_mutex_init(&lock, NULL);

    // Create threads
    pthread_t thread1, thread2;
    pthread_create(&thread1, NULL, thread_func, NULL);
    pthread_create(&thread2, NULL, thread_func, NULL);

    // Wait for threads to finish
pthread_join(thread1, NULL);
    pthread_join(thread2, NULL);

    // Destroy lock
pthread_mutex_destroy(&lock);

    return 0;
}

In this example, the pthread_mutex_lock() function is used to acquire the lock, and the pthread_mutex_unlock() process by releasing the lock. The shared resource accessed by the threads can be protected by the lock to ensure that only one thread accesses it at a time.


✔ Semaphores


Semaphores are another synchronization primitive that can be used to control access to shared resources. A semaphore is a counter that can be incremented or decremented by threads or processes. When a thread or process wants to access a shared resource, it first tries to decrement the semaphore. If the semaphore is greater than zero, the thread or process can access the resource. If the semaphore is zero, the thread or process is blocked until the semaphore is incremented by another thread or process.

Here is an example of how semaphores work in C:


 
 #include <pthread.h>
 #include <semaphore.h>
sem_t semaphore;
void* thread_func(void* arg) { 
// Wait for semaphore
 sem_wait(&semaphore);
// Access shared resource
// Signal semaphore
sem_post(&semaphore);

}
int main() { 
// Initialize semaphore 
sem_init(&semaphore, 0, 1);

// Create threads
pthread_t thread1, thread2;
pthread_create(&thread1, NULL, thread_func, NULL);
pthread_create(&thread2, NULL, thread_func, NULL);

// Wait for threads to finish
pthread_join(thread1, NULL);
pthread_join(thread2, NULL);

// Destroy semaphore
sem_destroy(&semaphore);

return 0;
}

In this example, the sem_wait() function is used to wait for the semaphore to be decremented, and the sem_post() function is used to increment the semaphore after accessing the shared resource. The semaphore ensures that only one thread accesses the shared resource at a time.


✔ Condition Variables


Condition variables are another synchronization primitive that can be used to coordinate the execution of multiple threads. A condition variable allows threads to wait until a particular condition is met before proceeding with their execution. Threads can signal the condition variable when they change the state of the shared resource, allowing other threads waiting on the condition variable to wake up and check the resource's state.

Here is an example of how condition variables work in C:


#include <pthread.h>
pthread_mutex_t mutex; 
pthread_cond_t cond; 
int shared_resource = 0;
void* thread_func(void* arg) { 
// Acquire lock 
pthread_mutex_lock(&mutex);

// Wait for condition variable
while (shared_resource == 0) {
    pthread_cond_wait(&cond, &mutex);
}

// Access shared resource
// Release lock
pthread_mutex_unlock(&mutex);

}
int main() { 
// Initialize mutex and condition variable 
pthread_mutex_init(&mutex, NULL); 
pthread_cond_init(&cond, NULL);

// Create threads
pthread_t thread1, thread2;
pthread_create(&thread1, NULL, thread_func, NULL);
pthread_create(&thread2, NULL, thread_func, NULL);

// Change shared resource state
shared_resource = 1;

// Signal condition variable
pthread_cond_broadcast(&cond);

// Wait for threads to finish
pthread_join(thread1, NULL);
pthread_join(thread2, NULL);

// Destroy mutex and condition variable
pthread_mutex_destroy(&mutex);
pthread_cond_destroy(&cond);

return 0;
}

In this example, the pthread_cond_wait() function is used to wait for the condition variable, and the pthread_cond_broadcast() function is used to signal the condition variable after changing the shared resource's state. The threads waiting on the condition variable wake up and check the resource's state, allowing them to access it.


📕 Posix Threads in OS


Introduction to POSIX Threads: POSIX Threads or Pthreads, is a POSIX standard for threads programming in C/C++ languages. Pthreads are used to create multiple threads within a single process that can execute independently and concurrently. These threads share the same process address space and can communicate and synchronize with each other using different synchronization primitives such as mutex, condition variables, and semaphores. Pthreads provide a portable and efficient way of creating parallel programs that can take advantage of multiple CPUs or cores.

In this tutorial, we will cover the following topics:

  1. Thread Creation

  2. Thread Termination

  3. Thread Synchronization

  • Mutexes

  • Condition Variables

  • Semaphores

✔ Thread Creation


Creating a new thread involves allocating resources such as stack space, thread ID, and CPU time. In POSIX threads, this is done using the pthread_create() function. Here is an example of creating a new thread:


#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
void *thread_function(void *arg){
    printf("Thread function is running. Argument was %d\n", *(int *)arg);
    return NULL;
}

int main(){
    pthread_t my_thread;
    int thread_arg = 42;

    if (pthread_create(&my_thread, NULL, thread_function, &thread_arg)) {
        printf("Error creating thread.\n");
        abort();
    }

    printf("Main thread waiting for newly created thread to terminate...\n");

    if (pthread_join(my_thread, NULL)) {
        printf("Error joining thread.\n");
        abort();
    }

    printf("Newly created thread has terminated.\n");
    return 0;
}

In this example, the main thread creates a new thread using pthread_create(). The new thread runs the function thread_function() and is passed an integer argument. The main thread waits for the new thread to terminate using pthread_join().


✔ Thread Termination


Terminating a thread involves releasing the resources allocated to it. In POSIX threads, this is done using the pthread_exit() function. Here is an example of terminating a thread:


#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
void *thread_function(void *arg){
    printf("Thread function is running. Argument was %d\n", *(int *)arg);
    pthread_exit(NULL);
}

int main(){
    pthread_t my_thread;
    int thread_arg = 42;

    if (pthread_create(&my_thread, NULL, thread_function, &thread_arg)) {
        printf("Error creating thread.\n");
        abort();
    }

    printf("Main thread waiting for newly created thread to terminate...\n");

    if (pthread_join(my_thread, NULL)) {
        printf("Error joining thread.\n");
        abort();
    }

    printf("Newly created thread has terminated.\n");
    return 0;
}

In this example, the new thread calls pthread_exit() to terminate itself.


✔ Thread Synchronization


Thread synchronization involves coordinating the execution of multiple threads to ensure that they don't interfere with each other.POSIX threads (pthreads) provide several mechanisms for thread synchronization, including mutexes, condition variables, and semaphores.

  • Mutexes: Mutexes (short for "mutual exclusion") are the most commonly used mechanism for thread synchronization. A mutex is a lock that threads can acquire and release to protect a shared resource. When a thread acquires a mutex, no other thread can access the protected resource until the mutex is released. Mutexes are used to protect shared data structures, critical sections of code, and other resources that should not be accessed simultaneously by multiple threads.

  • Condition Variables: Condition variables allow threads to synchronize and communicate with each other based on the state of shared variables. Condition variables are associated with a mutex to allow the waiting thread to atomically release the mutex while waiting for a condition to occur.

  • Semaphores: Semaphores are another synchronization primitive that can be used to control access to shared resources. A semaphore is a counter that can be incremented or decremented by threads or processes. When a thread or process wants to access a shared resource, it first tries to decrement the semaphore.

✔ Pthread library and functions


The pthread library is a library of functions in the C programming language that provides support for creating and managing threads. "pthread" stands for "POSIX threads", referring to the POSIX standard that defines the API for thread creation and synchronization.

The library includes functions for creating threads, synchronizing threads, and managing thread attributes, among other things. Some of the most commonly used functions in the pthread library include:

  • pthread_create(): This function is used to create a new thread. It takes a pointer to a pthread_t variable, which is used to identify the new thread, and a pointer to a function that will be executed by the new thread.

  • pthread_join(): This function is used to wait for a thread to complete its execution. It takes a pthread_t variable that identifies the thread to wait for.

  • pthread_mutex_init(): This function is used to initialize a mutex, which is a synchronization object that is used to protect shared resources from concurrent access by multiple threads.

  • pthread_mutex_lock(): This function is used to acquire a mutex, blocking the calling thread if the mutex is already held by another thread.

  • pthread_mutex_unlock(): This function is used to release a mutex, allowing other threads to acquire it.

There are many other functions in the pthread library, and they are all documented in the POSIX standard. To use the pthread library, you must include the <pthread.h> header file in your C program and link your program with the pthread library using the -lpthread flag.


Thanks for reading, and happy coding!


Synchronization Hardware and Posix Threads: Optimizing Multithreaded Applications ->

Understanding Process Scheduling in Linux


bottom of page