Thread synchronization mutual exclusion mutex, read-write lock, condition variable

1. Basic concepts

Mutex and synchronization are the most basic logical concepts:

  • Mutual exclusion refers to controlling two processes so that they are mutually exclusive and do not run at the same time.
  • Synchronization refers to controlling two progresses so that they come first and then come first, and the order is controllable.

2. Mutex basic logic

The easiest way to make mutual exclusion between multiple threads is to add a mutex. Any line that wants to start running the code in the mutex interval must first acquire the mutex lock, and the essence of the mutex lock is a binary semaphore, so when one of the threads preemptively acquires the mutex lock, the rest of the threads will be locked. It cannot be acquired again. The effect is equivalent to adding a lock to the relevant resource. Only when the user actively unlocks it can other threads have the opportunity to acquire the lock.

Mutex usage scenario: When we use some critical resources to prevent multiple threads from accessing at the same time, we can do this. Before accessing critical resources, let the threads lock first, then access the resources, and unlock them after accessing, so that Other threads go to lock. Description: Critical resources: shared resources (resources that need to be operated together between multiple threads)

The function interface description of the mutex is as follows:

1) Define mutex variable -> data type: pthread_mutex_t ---> global variable
pthread_mutex_t m;
2) Initialize the mutex -> pthread_mutex_init()
#include <pthread.h>
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);
parameter:
mutex: the address of the uninitialized mutex variable
    mutexattr: common attribute, NULL
return value:
Success: 0
    Failure: non-zero error code
Static initialization:
pthread_mutex_t m = PTHREAD_MUTEX_INITIALIZER;
That is, the above sentence is equivalent to:
pthread_mutex_t m;
pthread_mutex_init( &m,NULL);

3) Lock it. -> pthread_mutex_lock()
#include <pthread.h>
int pthread_mutex_lock(pthread_mutex_t *mutex);
parameter:
mutex: the address of the mutex variable
return value:
Success: 0
    Failure: non-zero error code
4) Unlock -> pthread_mutex_unlock()
#include <pthread.h>
int pthread_mutex_unlock(pthread_mutex_t *mutex);
parameter:
mutex: the address of the mutex variable
return value:
Success: 0
    Failure: non-zero error code
5) Destroy the mutex. -> pthread_mutex_destroy()
#include <pthread.h>
int pthread_mutex_destroy(pthread_mutex_t *mutex);
parameter:
mutex: the address of the mutex variable
return value:
Success: 0
    Failure: non-zero error code

The test code is as follows:

#include<stdio.h>
#include <sys/types.h>
#include <string.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <signal.h>
#include <sys/sem.h>
#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>
#include <semaphore.h>

int g_val = 10;

//1, first define a mutex variable
pthread_mutex_t mutex;
//Static initialization
//pthread_mutex_t m = PTHREAD_MUTEX_INITIALIZER;

void* start_routine1(void*arg)
{
//Lock first when accessing shared resources
pthread_mutex_lock( & mutex);

g_val = 20;
sleep(1);
printf("start_routine1 g_val:%d\
",g_val);

// Shared resources need to be unlocked at the end of use
pthread_mutex_unlock( & mutex);
}

void* start_routine2(void*arg)
{
//When accessing shared resources, lock it first -- if you don't get the lock, it will block and wait for the lock
pthread_mutex_lock( & mutex);

g_val = 200;
sleep(2);
printf("start_routine2 g_val:%d\
",g_val);

// Shared resources need to be unlocked at the end of use
pthread_mutex_unlock( & mutex);
}

int main()
{
//2. Initialize the mutex
pthread_mutex_init( & mutex, NULL);

//1, child thread 1
pthread_t thread1;
pthread_create( & thread1, NULL, start_routine1, NULL);

//2, child thread 2
pthread_t thread2;
pthread_create( & thread2, NULL, start_routine2, NULL);

pthread_join(thread1, NULL);
pthread_join(thread2, NULL);

//destroy the lock
pthread_mutex_destroy( & mutex);

return 0;
}

3. Basic logic of read-write lock

For mutexes, all accesses involving critical resources are locked, which will waste a lot of time in the scenario of concurrent read operations. In order to improve access efficiency, it is necessary to distinguish read and write operations on resources: read operations can be executed concurrently by multiple tasks, and only write operations can be properly mutually exclusive. This is the design source of the read-write lock.

1. Defects of mutex

Whether the mutex is to read shared resources or modify shared resources, it must be locked, but during the locking period, it cannot be locked by other threads.

2. Advantages of read-write lock

Access resources (read a book together) -> read lock at the same time -> read lock is a shared lock.

Modify resources -> cannot write lock at the same time -> write lock is a mutex.

This lock that has both a read lock and a write lock is called a read-write lock.

The function interface of the read-write lock is described as follows:

1) Define a read-write lock variable (data type: pthread_rwlock_t)
pthread_rwlock_t rwlock; (rwlock=read write lock)
2) Initialize the read-write lock----"man 3 pthread_rwlock_init
#include <pthread.h>
int pthread_rwlock_init(pthread_rwlock_t * rwlock, const pthread_rwlockattr_t * attr);
parameter:
rwlock: the address of the read-write lock variable
    attr: attribute, usually NULL
return value:
Success: 0
    Failure: non-zero error code
3) Read the lock and lock it. --->man 3 pthread_rwlock_rdlock (rdlock = read lock)
#include <pthread.h>
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
parameter:
rwlock: the address of the read-write lock variable
4) Write lock lock --- "pthread_rwlock_wrlock (wrlock = write lock)
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
parameter:
rwlock: the address of the read-write lock variable
5) Read-write lock unlock --- "pthread_rwlock_unlock (the unlock function of write lock and read lock is the same)
#include <pthread.h>
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
parameter:
rwlock: the address of the read-write lock variable
6) Destroy the read-write lock. ----"pthread_rwlock_destroy
#include <pthread.h>
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
parameter:
rwlock: the address of the read-write lock variable

Read-write lock test code is as follows:

#include <stdio.h>
#include <sys/types.h>
#include <string.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <signal.h>
#include <sys/sem.h>
#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>
#include <semaphore.h>

//Define a read-write lock variable
pthread_rwlock_t rwlock;

int g_val = 10;

void *start_routine1(void*arg)
{
pthread_rwlock_wrlock( & amp;rwlock);//add write lock

g_val = 20;
sleep(3);
printf("start_routine1: %d\
",g_val);

pthread_rwlock_unlock( & amp;rwlock); //unlock
}

void *start_routine2(void*arg)
{
pthread_rwlock_wrlock( & amp;rwlock);//add write lock

g_val = 200;
sleep(3);
printf("start_routine2: %d\
",g_val);

pthread_rwlock_unlock( & amp;rwlock); //unlock
}
void *start_routine3(void*arg)
{
pthread_rwlock_rdlock( & amp;rwlock);//add read lock

//get data access (read) shared resource but not modify (write)
int cnt=5;
while(cnt--){
sleep(1);
printf("start_routine3: %d\
",g_val);
}

pthread_rwlock_unlock( & amp;rwlock); //unlock
}

void *start_routine4(void*arg)
{
pthread_rwlock_rdlock( & amp;rwlock);//add read lock

// Get data access (read) shared resources but not modify (write)
int cnt=5;
while(cnt--){
sleep(1);
printf("start_routine4: %d\
",g_val);
}

pthread_rwlock_unlock( & amp;rwlock); //unlock
}

int main()
{
//Initialize read-write lock
pthread_rwlock_init( &rwlock,NULL);

pthread_t thread1;
pthread_create( & thread1, NULL, start_routine1, NULL);
pthread_t thread2;
pthread_create( & thread2, NULL, start_routine2, NULL);
pthread_t thread3;
pthread_create( & thread3, NULL, start_routine3, NULL);
pthread_t thread4;
pthread_create( & thread4, NULL, start_routine4, NULL);


pthread_join(thread1, NULL);
pthread_join(thread2, NULL);
pthread_join(thread3, NULL);
pthread_join(thread4, NULL);
   
//Destroy the read-write lock
    pthread_rwlock_destroy( & rwlock);

    return 0;
}

4. Basic concepts of condition variables

In many occasions, the execution of the program usually needs to meet certain conditions. When the conditions are immature, the task should enter sleep blocking waiting, and when the conditions are ripe, the task should be able to be quickly awakened. In addition, in a concurrent program, other tasks will access the condition at the same time, so the condition must be accessed in a mutually exclusive manner at any time. The conditional quantity is a logic mechanism that specifically solves the above scenarios.

Note that in the above statement, conditions and conditional quantities are two different things. The so-called condition refers to the preconditions required for the program to continue running, such as whether the file has been read, whether the memory is cleared, and other specific scene restrictions, while the conditional quantity ( That is, pthread_cond_t) is a synchronization mutex variable to be discussed in this courseware, which is dedicated to solving the above logic scenarios.

Description:

  1. Before making a conditional judgment, lock it first (to prevent concurrent access by other tasks)
  2. After the lock is successfully locked, determine whether the condition is allowed
    • If conditions permit, directly operate the critical resource, and then release the lock
    • If the condition is not allowed, enter the waiting queue of the condition quantity to sleep, and release the lock at the same time
  3. The task sleeping in the condition quantity can be woken up by other tasks. When waking up, re-determine whether the condition allows the program to continue to execute. Of course, it must be locked first.

The function interface description of the condition variable is as follows:

1) Define a condition variable first. -> Data type: pthread_cond_t
pthread_cond_t cond;
2) Initialize the condition variable
#include <pthread.h>
int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr);

pthread_cond_t cond = PTHREAD_COND_INITIALIZER; //Static initialization
parameter:
cond: the address of the condition variable
    cond_attr common attribute, NULL.
return value:
Success: 0
        Failure: non-zero error code
3) How to enter the condition variable and wait (two functions: 1. Blocking and waiting 2. Automatic unlocking)
int pthread_cond_wait(pthread_cond_t * cond, pthread_mutex_t * mutex);
parameter:
cond: the address of the condition variable
    mutex: The address of the mutex -> enter the condition variable and it will be automatically unlocked.
return value:
Success: 0
    Failure: non-zero error code
4) How to wake up the thread waiting in the condition variable? -> When the thread leaves the condition variable, it will be automatically locked.
#include <pthread.h>
int pthread_cond_broadcast(pthread_cond_t *cond);//Broadcast: wake up all threads waiting in the condition variable
int pthread_cond_signal(pthread_cond_t *cond);//unicast: randomly wake up a thread waiting in the condition variable
parameter:
cond: the address of the condition variable
return value:
Success: 0
    Failure: non-zero error code
5) Destroy the condition variable----"man 3 pthread_cond_destroy
#include <pthread.h>
int pthread_cond_destroy(pthread_cond_t *cond);
parameter:
cond: the address of the condition variable

5. The use of conditional quantities

Conditional quantities are generally used in conjunction with mutexes (or binary semaphores). Mutex locks provide the function of locking critical resources, and conditional quantities provide the function of blocking sleep and wakeup.

The sample code is as follows:

#include <stdio.h>
#include <sys/types.h>
#include <string.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <signal.h>
#include <sys/sem.h>
#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>
#include <semaphore.h>

/*
Exercise: There are 4 children, and the task of each child is to receive 1000 living expenses. Before going back to school, the parents deposit 2000 yuan in the bank card first,
2 threads exit after receiving 1000 yuan, and the other 2 threads wait in the condition variable, and the father pays 1000 more to wake up all the children
Get up to get the money, after a while, make another 1,000 yuan, and then wake up the last child to get up and get the money and leave for school.
*/

int g_money = 2000;
//Define a mutex variable
pthread_mutex_t mutex;
//Define a condition variable and initialize it statically
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

void *start_routine(void*arg)
{
printf("[%lu] child thread start\
", pthread_self());

pthread_mutex_lock( & amp;mutex);//Locking strength should be small

//When the condition is not met, enter the condition variable and wait
while(g_money<1000){
printf("There is no money, go to the condition variable and wait for the parents to send money and notify.....\
");
//Automatically unlock, and block waiting
pthread_cond_wait( &cond, &mutex);
printf("Parents have sent money and have notified me, the balance at this time: %d\
", g_money);
}

//Come here, it means you have money
g_money -= 1000;
printf("[%lu] sub-thread got money, bank card balance: %d\
", pthread_self(), g_money);
\t\t
pthread_mutex_unlock( & amp;mutex); //unlock

printf("[%lu] sub-thread end\
", pthread_self());

//Take the money and leave
pthread_exit(NULL);
}

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

pthread_t thread1; //i
pthread_create( & thread1, NULL, start_routine, NULL);
pthread_t thread2; //brother
pthread_create( & thread2, NULL, start_routine, NULL);
pthread_t thread3; //sister
pthread_create( & thread3, NULL, start_routine, NULL);
pthread_t thread4; //brother
pthread_create( & thread4, NULL, start_routine, NULL);


int cnt=5;
while(cnt--){
sleep(1);
printf("The main thread (parent) is about to send money...%d\
", cnt);
}
//Main thread (parent) play money
pthread_mutex_lock( & amp;mutex);//lock
g_money + =1000;
pthread_mutex_unlock( & amp;mutex); //unlock
pthread_cond_broadcast( & amp;cond);//Broadcast: wake up all threads waiting in the condition variable

cnt=5;
while(cnt--){
sleep(1);
printf("The main thread (parent) is about to send money...%d\
", cnt);
}
pthread_mutex_lock( & amp;mutex);//lock
g_money + =1000;
pthread_mutex_unlock( & amp;mutex); //unlock
pthread_cond_signal( & amp;cond);//Unicast: Randomly wake up a thread waiting in the condition variable

pthread_join(thread1, NULL);
pthread_join(thread2, NULL);
pthread_join(thread3, NULL);
pthread_join(thread4, NULL);


pthread_mutex_destroy( & mutex);
pthread_cond_destroy( & amp;cond);
return 0;
}

The knowledge points of the article match the official knowledge files, and you can further learn relevant knowledge