[Linux] Thread safety issues ① – Explanation of the principle of mutex locks & how to use mutex locks to achieve mutual exclusion of resource access (with diagrams and code implementation)

Thread safety is mainly divided into two aspects, namely resource access mutual exclusion and thread synchronization (thread cooperation)

In this blog, we mainly explain the aspect of resource access mutual exclusion.

Table of Contents

Why do we need to implement resource access mutual exclusion?

The classic mechanism to implement resource access mutual exclusion (atomic access) – mutex lock

Mutex lock related functions

Specific implementation of resource access mutual exclusion using mutex locks

Code

Result graphic


Why do we need to implement resource access mutual exclusion?

Let’s make a hypothetical scenario

Suppose there are two threads, thread A and thread B. There is a global variable named num, and the initial value of num is 0.

Now both threads have to perform a + 1 operation on the global variable num, and both have obtained time slices. Do you think the result must be 2?

Not sure, why? Let me explain it to you through a few assembly instructions.

First of all, we need to know how the statement ” num = num + 1 ; ” is implemented in the assembly language of the computer.

  1. mov eax, num #Put the value of num into the register eax
  2. add eax, 1 #Add the value in register eax + 1
  3. mov num, eax #Assign the value in eax to num

Assume that thread A and thread B obtain the initial value of num at the same time, that is, their first assembly instructions are performed at the same time. No matter who is faster or slower in the following two steps, the final result of num is 1, because num in their registers The values are all 0. Only when two threads modify the num value one after another can the correct result be obtained 2

Why does this happen? Just because they did not implement atomic access to resources, the two threads overwrote each other’s results. Therefore, we need to adopt methods to prevent this situation, achieve mutual exclusion of resource access, and allow two threads to achieve atomic access to these shared data. That is to say, when you read and write these shared data, other threads cannot read and write these data.

The classic mechanism for realizing mutual exclusion of resource access (atomic access) – mutex lock

One of the classic mechanisms to implement atomic access is the mutex lock mechanism. To understand the mutex lock mechanism, we can take the most common thing around us as an example, that is the bathroom

Let me make a scenario hypothesis to help you understand the mutex lock mechanism

Suppose there are ten people coming to the toilet one after another, and there is only one toilet

  1. The first person arrived and tried to open the bathroom door (actually, he was trying to obtain the right to use the bathroom door lock). He found that the door was not locked, so he obtained the right to use the bathroom door lock, locked the bathroom door and used bathroom
  2. The second person arrived and tried to open the bathroom door. He found that the door was locked, so he was the first in line to wait for the bathroom to open.
  3. Other people arrived, tried to open the bathroom door, and found that the door was locked. They lined up behind the first person and waited for the bathroom to open.
  4. The first person came out and unlocked the bathroom door. The bathroom can now be used by one person.
  5. The first person in line gets the right to use the bathroom door lock, locks the bathroom door and uses the bathroom. The following steps are the same as above.

The bathroom is like data, and the bathroom door lock is like a mutual exclusion lock.

If each thread wants to use these shared data, it must first try to obtain the mutex lock. If it is found that the bathroom door lock is locked, that is, the mutex lock is being used, these threads will enter the resource waiting queue and wait for this The right to use the lock. When the mutex lock can be used, the thread at the top will get the lock and lock the shared resources that need to be accessed, so that these shared resources can be used correctly.

PS:

  1. There can only be one mutex lock for the same shared resource. If there are multiple mutex locks and each thread can hold the mutex lock to lock the shared resource, the existence of the mutex lock will be meaningless.
  2. Shared resources between threads include: global resources (global variables are a type of global resources), file descriptors, process information, heap space, signal behavior, library space, etc.
  3. The code in the mutex protection area is also called critical section code. The shorter the critical section code, the better, otherwise it will affect work efficiency.
  4. The thread that gets the mutex lock may get the mutex lock again after unlocking it. It is not that a thread can only get the mutex lock once.

If you still can’t understand it, let’s still take the two threads above as examples and compare them with the above scenario.

Assume that there are two threads, namely thread A and thread B. To add one to the global variable num whose initial value is 0, the steps are as follows

Steps Bathroom scene Thread scene
The first person arrived and tried to obtain the right to use the bathroom door lock. He found that the door was not locked, so he obtained the bathroom door lock. The right to use the door lock, lock the bathroom door and use the bathroom Thread A arrived, tried to obtain the mutex lock, and found that the lock was not used, so it obtained Obtain the right to use the lock and lock the global variable num
The second person arrived and tried to open the bathroom door, but found that the door was locked, so he was the first in line to wait for the bathroom to open Thread B arrived and tried to acquire the mutex lock, but there was only one lock, so it entered the resource waiting queue to wait for the right to use the lock
The first person came out and unlocked the bathroom door. The bathroom door lock can now be used by one person Thread A completes the + 1 operation on the global variable num and unlocks the mutex lock. The lock can now be used by a thread
The second person obtained the right to use the bathroom door lock and locked the bathroom door And use the bathroom Thread B obtained the right to use the mutex lock and locked the global variable num
The second person came out and unlocked the bathroom door, and the bathroom was used. Twice, the scenario is completed Thread B completes the + 1 operation of the global variable num, unlocks the mutex lock, and the result of num is 2. The scenario Complete

The above is roughly the specific process of implementing the mutex lock mechanism. Next, let’s learn about the following related functions.

Here we need to use a structure called pthread_mutex_t, which is the related structure of the mutex lock

Let’s first introduce some variables that will be used in the next moment:

  • pthread_mutex_t lock; //Define a mutex lock structure
  • const pthread_mutexattr_t attr; //Lock attribute related structure, if you use the default attribute, just pass NULL directly
Function Function Return value
pthread_mutex_init( & amp;lock , & amp;attr); Implement the initialization of mutex lock 0 will be returned after successful completion, any other return value indicates an error
pthread_mutex_destroy( & amp;lock); Release the memory occupied by the lock resource Return 0 successfully , failure returns error number
pthread_mutex_lock( & amp;lock); Lock Returns 0 on success, error number on failure
pthread_mutex_unlock( & amp;lock); Unlock Return successfully 0, error number returned on failure

A specific implementation of using a mutex lock to achieve mutual exclusion of resource access

After understanding the relevant functions and implementing mutually exclusive access to resources, let’s write a small code to implement a function:

  • Each of the two threads adds 5000 times to the global variable num, adding 1 each time (difficulty:)

PS: It should be noted that instead of each thread filling up 5000 times at a time, the other thread can add 5000 times, but the two threads take turns adding up, and finally both can fill up 5000 times.

Code Implementation

//mutex_lock.c

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <signal.h>
#include <pthread.h>
#include <fcntl.h>

//Define the number of times as a macro
#define CONT 5000

//Define the mutex lock and num as global variables so that both threads can acquire the lock and num
pthread_mutex_t lock;
int num = 0;

//The work of the two threads is the same, so only one work function needs to be defined here
void* thread_job(void* arg)
{
    int i = 0;
    pthread_detach(pthread_self());//Set the thread to a detached state and let the system recycle it by itself
    //Loop + 1 operation on num
    //Note, do not add the lock outside the loop. If it is placed outside, it means that one thread can be filled 5000 times at a time before another thread can be added.
    //If the lock is placed outside the loop, the working efficiency of the two threads is not as high as that of the single thread.
    while(i < CONT)
    {
        pthread_mutex_lock( & amp;lock);//Lock
        //This is the critical section code. The code between locking and unlocking is the critical section code.
        num + + ;
        i + + ;
        printf("thread No.0x%x + + num , num = %d\\
" , (unsigned int)pthread_self() , num);
        pthread_mutex_unlock( & amp;lock);//Unlock
    }
    pthread_exit(NULL);
}

int main()
{
    //Initialize mutex lock
    pthread_mutex_init( & amp;lock , NULL);
    pthread_t tids[2];
    //Create thread A and thread B
    pthread_create( & amp;tids[0] , NULL , thread_job , NULL);
    pthread_create( & amp;tids[1] , NULL , thread_job , NULL);
    //Let the main thread sleep in a loop to let thread A and thread B obtain time slices
    //Since we set the thread to a detached state, the system will automatically recycle the thread without us having to worry about it.
    while(1)
    {
        sleep(1);
    }
    //Recycle lock resources
    pthread_mutex_destroy( & amp;lock);
    exit(0);
}

result icon

We can find that the above code implements this function and completes our requirements. It does not allow a thread to fill up 5,000 times at a time. When it reaches 9310, a thread conversion occurs.

The above is the entire content of this blog. If you don’t understand anything, you can leave me a message in the comment area and I will help you answer it to the best of my ability.

Today’s learning record ends here. See you in the next article, ByeBye!