[OS series-3] - thread explanation - Communication and synchronization

Inter thread synchronization

  • Locking mechanism: mutex, conditional variable, semaphore, read / write lock
  • Mutex: provides a way for data structures to be modified concurrently in an exclusive manner
  • Read / write lock: write lock preempts resources first. Read lock allows multiple threads to read shared data together, and write lock operations are mutually exclusive
  • Condition variable: blocks the process atomically until a particular condition is true

In general, mutexes are used for protection, and condition variables are used together with mutexes

Mutex (mutex)

Locking mechanism is to allow only one thread to execute a key part of the code at the same time.

C++
//Initialization lock
int pthread_mutex_init(pthread_mutex_t *mutex,const pthread_mutex_attr_t *mutexattr);

The parameter mutexattr is used to specify the attribute of the lock (see below). If it is NULL, the default attribute is used.

The attribute of a mutex is specified when creating a lock. In the implementation of Linux threads, there is only one lock type attribute. Different lock types behave differently when trying to lock a locked mutex. There are currently four values to choose from:

  • (1)PTHREAD_MUTEX_TIMED_NP, which is the default value, that is, ordinary lock. When a thread is locked, the other threads requesting the lock will form a waiting queue and obtain the lock according to priority after unlocking. This locking strategy ensures the fairness of resource allocation.

  • (2)PTHREAD_MUTEX_RECURSIVE_NP, nested lock, allows the same thread to successfully obtain the same lock multiple times and unlock it through multiple unlocks. If it is a different thread request, it will compete again when the locked thread is unlocked.

  • (3)PTHREAD_MUTEX_ERRORCHECK_NP, error detection lock. If the same thread requests the same lock, it returns EDEADLK. Otherwise, it is the same as PTHREAD_MUTEX_TIMED_NP type actions are the same. This ensures that deadlock in the simplest case will not occur when multiple locking is not allowed.

  • (4)PTHREAD_MUTEX_ADAPTIVE_NP, adaptive lock, the simplest lock type, only wait for unlocking and compete again.

//Blocking and locking
int pthread_mutex_lock(pthread_mutex *mutex);

//Non blocking locking

//This function has the same semantics as pthread_mutex_lock() is similar, except that EBUSY is returned when the lock has been occupied instead of pending.
int pthread_mutex_trylock( pthread_mutex_t *mutex);

//Destroy lock
int pthread_mutex_destroy(pthread_mutex *mutex);

Deadlock of mutex:

A thread needs to access two or more different shared resources, and each resource has different mutex management. When more than one thread locks the same set of mutexes, deadlock may occur.
Deadlock refers to a deadlock (mutual waiting) caused by multiple threads / processes competing for resources. Without external force, these processes will not be able to move forward.

Deadlock handling strategy:

  1. Deadlock prevention: break the four conditions for deadlock generation: mutual exclusion condition, non deprivation condition, request and hold condition and circular waiting condition.
  2. Deadlock avoidance: before each resource allocation, the security of the allocated resources should be calculated. If the resource allocation will not cause the system to enter an unsafe state, the resources should be allocated to the process, otherwise wait. Algorithm: banker algorithm.
  3. Deadlock detection: after a deadlock is detected, the deadlock is removed by means of resource deprivation, process revocation, process fallback, etc.

Read write lock

Read / write locks are similar to mutexes, but read / write locks have higher parallelism. Mutexes are either locked or unlocked, and only one thread can lock them at a time. There are three states of read-write lock: lock in read mode, lock in write mode and no lock. Only one thread can occupy the read-write lock of write mode at a time, but multiple threads can occupy the read-write lock of read mode at the same time.

When the read-write lock is in the write lock state, all view threads that lock the lock will be blocked before the lock is unlocked. When the read-write lock is in the read lock state, all threads trying to lock it in read mode can get access, but any thread that wants to lock it in write mode will block until all threads release their read locks.

C++
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>

/* Initialize read / write lock */
pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;/* Global resources */
int global_num = 10;
void err_exit(const char *err_msg)
{
 printf("error:%s\n", err_msg);
 exit(1);
}

/* Read lock thread function */
void *thread_read_lock(void *arg)
{
 char *pthr_name = (char *)arg;

 while (1)
 {
     /* Read and lock */
     pthread_rwlock_rdlock(&rwlock);

     printf("thread %s Entering the critical zone, global_num = %d\n", pthr_name, global_num);
     sleep(1);
     printf("thread %s Leave the critical zone...\n", pthr_name);

     /* Read unlock */
     pthread_rwlock_unlock(&rwlock);
     sleep(1);
 }

 return NULL;

}

/* Write lock thread function */
void *thread_write_lock(void *arg)
{
 char *pthr_name = (char *)arg;

 while (1)
 {

     /* Write lock */
     pthread_rwlock_wrlock(&rwlock);

     /* Write operation */
     global_num++;
     printf("thread %s Entering the critical zone, global_num = %d\n", pthr_name, global_num);
     sleep(1);
     printf("thread %s Leave the critical zone...\n", pthr_name);

     /* Write unlock */
     pthread_rwlock_unlock(&rwlock);

     sleep(2);

 }

 return NULL;
}
int main(void)
{
 pthread_t tid_read_1, tid_read_2, tid_write_1, tid_write_2;

 /* Create 4 threads, 2 reads and 2 writes */
 if (pthread_create(&tid_read_1, NULL, thread_read_lock, "read_1") != 0)
     err_exit("create tid_read_1");

 if (pthread_create(&tid_read_2, NULL, thread_read_lock, "read_2") != 0)
     err_exit("create tid_read_2");

 if (pthread_create(&tid_write_1, NULL, thread_write_lock, "write_1") != 0)
     err_exit("create tid_write_1");

 if (pthread_create(&tid_write_2, NULL, thread_write_lock, "write_2") != 0)
     err_exit("create tid_write_2");

 /* Just wait for a thread to prevent main from ending */
 if (pthread_join(tid_read_1, NULL) != 0)
     err_exit("pthread_join()");

 return 0;

}

Conditional variable (cond)

Conditional variable is a mechanism for synchronization by sharing global variables among threads.
The basic operations on condition variables are: trigger condition (when the condition becomes true); Wait for the condition to suspend the thread until another thread triggers the condition.

Conditional variables are another synchronization mechanism available to threads. Mutexes are used for locking, and condition variables are used for waiting. Condition variables always need to be used with mutexes. Running threads wait for specific conditions in a non competitive way.
The condition variable itself is protected by the mutex. The thread must first lock the mutex before changing the condition variable. Other threads will not notice this change until they get the mutex, because the mutex must be locked before the condition can be evaluated.

C++
//Initial call condition variable
int pthread_cond_init(pthread_cond_t *cond,pthread_condattr_t *cond_attr);

//Unconditional waiting
int pthread_cond_wait(pthread_cond_t *cond,pthread_mutex_t *mutex);

//Timing wait
//If the conditions are not met before the given timing, ETIMEOUT is returned to end the wait, where abstime appears in the form of absolute time with the same meaning as the time() system call. 0 means 0:0:0 GMT on January 1, 1970.

int pthread_cond_timewait(pthread_cond_t *cond,pthread_mutex *mutex,const timespec *abstime);

Either wait mode must be matched with a mutex to prevent multiple threads from requesting at the same time (with pthread_cond_wait() or pthread)_ cond_ Timedwait() request Race Condition.

Mutex mutex must be a normal lock (PTHREAD_MUTEX_TIMED_NP) or an adaptive lock (PTHREAD_MUTEX_ADAPTIVE_NP) and pthread is called_ cond_ Before wait(), this thread must lock (pthread_mutex_lock()). Before updating the conditional wait queue, mutex remains locked and unlocked before the thread is suspended and enters the wait. Leave pthread when conditions are met_ cond_ Before wait (), mutex will be re locked to enter pthread_ cond_ Corresponding to the locking action before wait().

C++
//Activate a thread waiting for this condition (activate one of them in queue order when there are multiple waiting threads)
int pthread_cond_signal(pthread_cond_t *cond);

//Activate all waiting threads
int pthread_cond_broadcast(pthread_cond_t *cond);

//Destroy condition variable
//This condition variable can be destroyed only when no thread is waiting on it. Otherwise, EBUSY is returned
int pthread_cond_destroy(pthread_cond_t *cond);

Semaphore

Like processes, threads can communicate through semaphores, although they are lightweight.

Thread semaphores are similar to process semaphores. Using thread semaphores can efficiently complete thread based resource counting. Semaphore is actually a non negative integer counter used to control common resources. When public resources increase, the semaphore increases; When public resources decrease, the semaphore decreases; Only when the value of the semaphore is greater than 0 can the public resources represented by the semaphore be accessed.

There are four basic semaphore functions used by threads:

C++
//Initialization semaphore

/*//
sem - specifies the semaphore to initialize;
pshared - semaphore sem sharing option. linux only supports 0, indicating that it is a local semaphore of the current process;
Value - initial value of semaphore sem.
*/

int sem_init (sem_t *sem , int pshared, unsigned int value);

//Semaphore value plus 1
int sem_post(sem_t *sem);

//Semaphore value minus 1
//If the semaphore value indicated by sem is 0, the function will wait until another thread makes it no longer 0.
int sem_wait(sem_t *sem);

//Destroy semaphore
int sem_destroy(sem_t *sem);

Sample code

// C++
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#include <semaphore.h>
#include <errno.h>

#define return_if_fail(p) if((p) == 0){printf ("[%s]:func error!\n", __func__);return;}


typedef struct _PrivInfo
{
    sem_t s1;
    sem_t s2;
    time_t end_time;
}PrivInfo;

static void info_init (PrivInfo* prifo);
static void info_destroy (PrivInfo* prifo);
static void* pthread_func_1 (PrivInfo* prifo);
static void* pthread_func_2 (PrivInfo* prifo);


int main (int argc, char** argv)
{
    pthread_t pt_1 = 0;
    pthread_t pt_2 = 0;
    int ret = 0;
    PrivInfo* prifo = NULL;
    prifo = (PrivInfo* )malloc (sizeof (PrivInfo));


    if (prifo == NULL) {
        printf ("[%s]: Failed to malloc priv.\n");
        return -1;
    }

    info_init (prifo);
    ret = pthread_create (&pt_1, NULL, (void*)pthread_func_1, prifo);
    if (ret != 0) {
        perror ("pthread_1_create:");
    }
    ret = pthread_create (&pt_2, NULL, (void*)pthread_func_2, prifo);
    if (ret != 0) {
        perror ("pthread_2_create:");
    }

    pthread_join (pt_1, NULL);
    pthread_join (pt_2, NULL);
    info_destroy (prifo);
    return 0;

}

static void info_init (PrivInfo* prifo)
{
    return_if_fail (prifo != NULL);
    prifo->end_time = time(NULL) + 10;
    sem_init (&prifo->s1, 0, 1);
    sem_init (&prifo->s2, 0, 0);
    return;

}

static void info_destroy (PrivInfo* prifo)
{
    return_if_fail (prifo != NULL);
    sem_destroy (&prifo->s1);
    sem_destroy (&prifo->s2);
    free (prifo);
    prifo = NULL;
    return;
}

static void* pthread_func_1 (PrivInfo* prifo)
{
    return_if_fail (prifo != NULL);
    while (time(NULL) < prifo->end_time)
    {
        sem_wait (&prifo->s2);
        printf ("pthread1: pthread1 get the lock.\n");
        sem_post (&prifo->s1);
        printf ("pthread1: pthread1 unlock\n");
        sleep (1);
    }
    return;
}

static void* pthread_func_2 (PrivInfo* prifo)
{
    return_if_fail (prifo != NULL);
    while (time (NULL) < prifo->end_time)
    {
        sem_wait (&prifo->s1);
        printf ("pthread2: pthread2 get the unlock.\n");
        sem_post (&prifo->s2);
        printf ("pthread2: pthread2 unlock.\n");
        sleep (1);
    }
    return;
}

The difference between mutual exclusion and synchronization

Mutual exclusion: it means that only one visitor is allowed to access a resource at the same time, which is unique and exclusive. However, mutual exclusion cannot limit the access order of visitors to resources, that is, the access is out of order.

Synchronization: it is mainly a process concept. It refers to the orderly access of visitors to resources through other mechanisms on the basis of mutual exclusion (in most cases). In most cases, synchronization has achieved mutual exclusion, especially when all written resources must be mutually exclusive. In a few cases, multiple visitors can be allowed to access resources at the same time.

Differences between mutexes, conditional variables, and semaphores

Mutex: mutex. If a thread occupies a certain resource, other threads cannot access it until the thread is unlocked.

Condition variable: synchronization. When a thread completes an action, it sends a signal to other threads through the condition variable, and other threads perform some actions. Condition variables must be used with mutexes.

Semaphore: synchronization. When a thread completes an action, it tells other threads through the semaphore, and other threads perform some actions. Moreover, semaphores have a more powerful function. Semaphores can be used as resource counters to initialize the value of semaphores to the currently available quantity of a resource, decrease after one is used, and increase after one is returned.

In addition, the following points need to be noted:

  1. Semaphores can simulate conditional variables, because conditional variables and mutexes are used together, which is equivalent to semaphores simulating the combination of conditional variables and mutexes. In the producer consumer thread pool, the producer will send a signal pthread after producing data_ cond_ Signal notifies the consumer thread through pthread_cond_wait for the signal to continue. This is the synchronization of producer and consumer threads with conditional variables and mutexes, which can be realized with semaphores!

  2. Semaphores can simulate mutexes, because mutexes can only be locked or unlocked (0 or 1), and semaphore values can be non negative integers. In other words, a mutex can only be used for mutex access of one resource, which can not realize multi-threaded mutex of multiple resources. Semaphores can realize multi-threaded mutual exclusion and synchronization of multiple similar resources. When the semaphore is a single valued semaphore, the mutually exclusive access of a resource is completed. As mentioned earlier, semaphores are mainly used for synchronization between multithreads and multitasks, and synchronization can control the process of thread access. When the semaphore is a single value, threads must be released before other threads can obtain it. Only one thread is running at the same time (note that this operation is not necessarily to access resources, but may be calculation). If the thread is accessing a resource, it is equivalent to mutually exclusive access to the resource.

  3. Mutex locks are optimized for locking; Condition variables are optimized for waiting; Semaphores can be used for both locking and waiting, so there will be more overhead and higher complexity.

  4. Mutexes and conditional variables are only used between threads of the same process, and semaphores (named semaphores) can be used for synchronization between different processes. When semaphores are used for inter process synchronization, they are required to be established in the shared memory area.

  5. Mutexes must be obtained and released by the same thread. Semaphores and condition variables can be released by one thread and obtained by another thread.

  6. The increase and decrease of semaphore will be automatically remembered by the system. The internal counter of the system realizes semaphore without worrying about loss. When waking up a condition variable, if there is no corresponding thread waiting for the condition variable, the wake-up will be lost.

Comparison between multithreading and multiprocessing

Multi process

  • advantage:
  1. Process memory space is independent;

  2. The additional protection and higher-level communication mechanism provided by the operating system between processes are more secure;

  3. Multi process, which can realize multi machine independent process based on network

  • Disadvantages:
  1. Each process has independent memory space, large inherent overhead and complex management;

  2. Complex interprocess communication settings;

Multithreading

  • advantage:
  1. All threads share the same address space with low system overhead;
  • Disadvantages:
  1. The disadvantage of shared memory space is the reduction of security;

deadlock

Deadlock refers to the blocking caused by two or more execution units waiting for each other to finish. For example:

A thread T1 obtains access to resource R1.

A thread T2 obtains access to resource R2.

T1 requests access to R2, but has to wait because this right is occupied by T2.

T2 requests access to R1, but has to wait because this right is occupied by T1.

T1 and T2 will always maintain the waiting state. At this time, we are in a deadlock! This problem is more hidden than most bug s you encounter. There are three main solutions to this problem:

  • One thread is not allowed to access multiple resources at the same time.
  • Define a relational order for obtaining access to resources. In other words, when a thread has obtained access to R1, it will not be able to obtain access to R2. Of course, the release of access rights must follow the reverse order.
  • Systematically define a maximum waiting time (timeout) for all requests to access resources, and properly handle the failure of requests. Almost all NET synchronization mechanism provides this function.

statement
This article is a personal study and summary of notes and feelings, involving network materials, excerpts from relevant books, personal summary and perception. There must also be omissions and unmarked ones. If there is infringement, please contact to delete.

Keywords: Java Back-end Operating System

Added by saeeddeep on Sun, 16 Jan 2022 11:56:30 +0200