Wednesday, April 29, 2020

Mutex Locks

Mutex lock is the popular way of achieving the thread synchronization. A mutex is lock that we set before using a shared resource and release it after using it. When the lock is set no other thread can use the shared resource or the locked code of region.

The usual routine of using a Mutex Locks has the following steps
  1. Initialize a mutex using the pthread_mutex_init function
  2. Lock a mutex either using the pthread_mutex_lock function or with pthread_mutex_trylock function
  3. Unlock a mutex using the pthread_mutex_unlock function
  4. Finally destroy the mutex using the pthread_mutex_destroy function.

Mutex Initialization

pthread_mutex_init() initializes the mutex to its default value or to specific mutex attributes if they have been already set up with the pthread_mutexattr_init().

Syntax

int pthread_mutex_init(pthread_mutex_t *mp, const pthread_mutexattr_t *mattr);
  • mp - pointer to the specific mutex
  • mattr - pointer to the mutex attribute. The default value is NULL. The effect of mattr set to NULL is the same as passing the address of a default mutex attribute object, but without the memory overhead.
When the mutex is initialized, the mutex is in an unlocked state. Do not reinitialize or destroy a mutex lock while other threads are using the mutex. Program failure results if either action is not done correctly. If a mutex is reinitialized or destroyed, the application must be sure the mutex is not currently in use.

Return Values

pthread_mutex_init() returns zero after completing successfully. Any other return value indicates that an error occurred. When any of the following conditions occurs, the function fails and returns the corresponding value.
  • EBUSY - The implementation has detected an attempt to reinitialize the object referenced by mp, a previously initialized but not yet destroyed mutex.
  • EINVAL - The mattr attribute value is invalid. The mutex has not been modified.
  • EFAULT - The address for the mutex pointed at by mp is invalid.
Use the macro PTHREAD_MUTEX_INITIALIZER to initialize statically defined mutexes to their default attributes.

Example

pthread_mutex_t mp = PTHREAD_MUTEX_INITIALIZER;

Mutex Locking

Use pthread_mutex_lock() to lock the mutex pointed to by mutex.

Syntax

int pthread_mutex_lock(pthread_mutex_t *mutex);
  • mutex - When pthread_mutex_lock() returns, the mutex is locked. 
The calling thread is the owner. If the mutex is already locked and owned by another thread, the calling thread blocks until the mutex becomes available. 

If the mutex type is PTHREAD_MUTEX_NORMAL, deadlock detection is not provided. Attempting to relock the mutex causes deadlock. If a thread attempts to unlock a mutex not locked by the thread or a mutex that is unlocked, undefined behavior results.

If the mutex type is PTHREAD_MUTEX_ERRORCHECK, then error checking is provided. If a thread attempts to relock a mutex that the thread has already locked, an error is returned. If a thread attempts to unlock a mutex not locked by the thread or a mutex that is unlocked, an error is returned.

If the mutex type is PTHREAD_MUTEX_RECURSIVE, then the mutex maintains the concept of a lock count. When a thread successfully acquires a mutex for the first time, the lock count is set to 1. Every time a thread relocks this mutex, the lock count is incremented by 1. Every time the thread unlocks the mutex, the lock count is decremented by 1. When the lock count reaches 0, the mutex becomes available for other threads to acquire. If a thread attempts to unlock a mutex not locked by the thread or a mutex that is unlocked, an error is returned.

If the mutex type is PTHREAD_MUTEX_DEFAULT, attempting to recursively lock the mutex results in undefined behavior. Attempting to unlock the mutex if the mutex was not locked by the calling thread results in undefined behavior. Attempting to unlock the mutex if the mutex is not locked results in undefined behavior.

Return Values

pthread_mutex_lock() returns zero after completing successfully. Any other return value indicates that an error occurred. When any of the following conditions occurs, the function fails and returns the corresponding value.
  • EAGAIN - The mutex could not be acquired because the maximum number of recursive locks for mutex has been exceeded.
  • EDEADLK - The current thread already owns the mutex
  • EOWNERDEAD - The last owner of this mutex failed while holding the mutex. This mutex is now owned by the caller. The caller must attempt to make the state protected by the mutex consistent. If the caller is able to make the state consistent, call pthread_mutex_consistent_np() for the mutex and unlock the mutex. Subsequent calls to pthread_mutex_lock() behave normally. If the caller is unable to make the state consistent, do not call pthread_mutex_init() for the mutex. Unlock the mutex instead. Subsequent calls to pthread_mutex_lock() fail to acquire the mutex and return an ENOTRECOVERABLE error code. If the owner that acquired the lock with EOWNERDEAD fails, the next owner acquires the lock with EOWNERDEAD.
  • ENOTRECOVERABLE - The mutex you are trying to acquire is protecting state left irrecoverable by the mutex’s previous owner that failed while holding the lock. The mutex has not been acquired. 
  • ENOMEM - The limit on the number of simultaneously held mutexes has been exceeded.

Thread Synchronization

Thread synchronization is the concurrent execution of two or more threads that share critical resources. Threads should be synchronized to avoid critical resource becoming conflict. A conflict may arise when parallel-running threads attempt to modify a common variable at the same time. 

In other words it is defined as a mechanism which ensures that two or more concurrent processes or threads do not simultaneously execute some particular program segment known as a critical section. Access to critical section is controlled by using synchronization techniques. 

Consider the following example: three threads - A, B, and C - are executed concurrently and need to access a critical resource, Z. To avoid conflicts when accessing Z, threads A, B, and C must be synchronized. Thus, when A accesses Z, and B also tries to access Z, B’s access of Z must be avoided with security measures until A finishes its operation and comes out of Z. 

In pthreads the following 4 types of synchronization methods are available
  • Mutex Locks
  • Condition variables
  • Read-Write Locks
  • Semaphores
I will deal with each of these synchronization techniques in separate posts.

Thread Join

The pthread_join function blocks the calling thread until the specified thread terminates.

Syntax

int pthread_join(thread_t tid, void **status);

  • tid - The function blocks the calling thread, until the thread id specified in this terminates.
  • status - When status is not NULL, status points to a location that is set to the exit status of the terminated thread when pthread_join() returns successfully.
If multiple threads wait for the same thread to terminate, all the threads wait until the target thread terminates. Then one waiting thread returns successfully. The other waiting threads fail with an error of ESRCH. After pthread_join() returns, any data storage associated with the terminated thread can be reclaimed by the application.

Return Values

pthread_join() returns zero when the call completes successfully. Any other return value indicates that an error occurred. When any of the following conditions are detected, pthread_join() fails and returns the corresponding value.
  • ESRCH - No thread could be found corresponding to the given thread ID.
  • EDEADLK - A deadlock would exist, such as a thread waits for itself or thread A waits for thread B and thread B waits for thread A.
  • EINVAL - The thread corresponding to the given thread ID is a detached thread. pthread_join() works only for target threads that are non-detached. 

Example

#include<stdio.h>
#include<pthread.h>

void * my_func(void * args)
{
        printf("My thread function called\n");
}

void main()
{
        pthread_t pid;
        int ret;

        ret = pthread_create(&pid, NULL, &my_func, NULL);
        if(ret != 0)
                printf("pthread_create failed\n");

        ret = pthread_join(pid,NULL);
        if(ret != 0)
                printf("pthread_join failed\n");
}

A simple thread creation

A pthread can be created using the function pthread_create(). It  adds a new thread of control to the current process.

Syntax

int pthread_create(pthread_t *pid, const pthread_attr_t *tattr, void*(*start_routine)(void *), void *arg);
  • pid - When pthread_create() is successful, the ID of the created thread is stored in the location referred to as pid.
  • start_routine - It is the function with which the new thread begins execution.
  • tattr - It has the necessary state behavior.
  • arg - If we want to pass any arguments to the thread function, then this parameter can be used

Return Values

pthread_create() returns zero when the call completes successfully. Any other return value indicates that an error occurred. When any of the following conditions are detected, pthread_create() fails and returns the corresponding value.
  • EAGAIN - A system limit is exceeded, such as when too many threads have been created.
  • EINVAL - The value of tattr is invalid.

Example:

#include<stdio.h>
#include<pthread.h>

void * my_func(void * args)
{
        printf("My thread function called\n");
}

void main()
{
        pthread_t pid;
        int ret;

        ret = pthread_create(&pid, NULL, &my_func, NULL);
        if(ret != 0)
                printf("pthread_create failed\n");

} 
 
To compile the above program, we need to use the option -lpthread.


When the above program is executed one would expect the output to print "My thread function called" but it will not. It is because before the thread gets executed our calling thread gets exited. To overcome this we have to use the pthread_join function.

Multi Threading Basics

Multi Processing & Multi Threading

A multi processing/multi tasking OS can have more than one process executing at any given time. This simultaneous execution may be possible either by
  • Concurrent, meaning that multiple processes in a run state can be swapped in and out by the OS
  • Parallel, meaning that multiple processes are actually running at the same time on multiple processors.
Just like the multitasking OS, a single process can have multiple threads that are executing in concurrent or parallel, which is called as the multi threading.

Thread

A thread is a single sequential flow of control within a program.  Each process by default contains one thread. A thread is sometimes called as lightweight process, since it has its own thread id, stack, stack pointer, signal mask, program counter, registers etc.

All threads within a given process share resource handles, memory segments (heap and data segments) and code. The following diagram shows the process vs thread 

As threads are lightweight, they can be created and managed more quickly than the process because
  • Threads have less overhead than process, for example threads share the heap, all data and code segments.
  • Processes don't need to be swapped for creating a thread.

Advantages of Multi Threading

  • Since the codes are running in concurrent or parallel, the speed of executing and the performance increases.
  • Increases application responsiveness.
  • Avoids inter process communications
  •  

POSIX Threads

The POSIX stands for Portable Operating System Interface. It is a family of standard specified for maintaining the compatibility between Operating System. It defines the application programming interface (API) along with command line shells and utility interfaces.

In the implementation of threads also different OS had it's own library and style, making the system incompatible. So POSIX defined the standards for the threads which are generally called as the POSIX Threads or the pthread.

Life cycle of thread

The thread life cycle consists of 4 stages. They are
  • ready - When a thread is ready to run, i.e. waiting for a processor then it is said to be in ready state.
  • running - When the thread is scheduled and is running/executing.
  • blocked - When the thread is waiting for a shared resource.
  • terminated - When the thread system resources are fully released and the thread becomes obsolete.