[Linux] Implementation of the Mutex Principle

Deep understanding of mutex

Article Directory


foreword

In order to realize mutual exclusion lock operation , most architectures provide swap or exchange instruction , the function of this instruction is to exchange the data of register and memory unit, since there is only one instruction , atomicity is guaranteed , even for multi-processor platforms , The bus cycle of accessing the memory also has a sequence . When the exchange instruction on one processor is executed, the exchange instruction of another processor can only wait for the bus cycle. As shown below:

 

 For the lock and unlock assembly code in the above figure, who is executing it? The answer is the calling thread.

 The assembly code circled here means: exchange shared data into its own private context. What does this mean? Let's explain in detail:

First of all, after our thread comes in, it locks and writes 0 to its own context, which is the code in the above figure. Here we must remember that when we switch threads in the future, we will take away the 0 in this context, because we define The lock is stored in memory. Since it is memory, the lock is destined to be shared. Then we execute the next instruction, which is the red part in the penultimate picture. Here is to exchange the value in the register with the content in the mutex variable. In the past, our register was 0 and the memory was 1, but now it has become 1,0 as shown in the figure below:

 This also proves what we just said about exchanging shared data into its own private context, which is the principle of locking.

Enter the if statement at this time, because we have just exchanged with the register, so this time the direct lock is successfully returned to 0. Let us mention here, what happens if the thread is switched before entering the if judgment statement? At this time, the first thread will take away its own context, that is to say, the 1 in the register is gone and taken away by the thread, as shown in the following figure:

 At this time, the second thread comes in, and the second process also needs to apply for a lock, so first exchange with the mutex in the memory, because the 1 of the mutex has been taken away by the first thread just now, so after exchanging registers and mutex, it is still 0. At this time, enter the if judgment statement and find that it is not greater than 0, you can only hang up and wait. This is the case for subsequent threads that apply for locks, because 1 (key) has been taken away by the first thread! ! At this time, the operating system takes the first thread, and finds that the content in the register is 1 greater than 0, and then successfully applies for the lock. Therefore, the 1 of the above mutex can only be transferred, and no 1 will be added. Unlocking is also very simple, just change the mutex in the execution flow to 1, because there is only one instruction for unlocking, so it is equivalent to atomic unlocking. 


1. Thread encapsulation of the demo version

class Thread
{
public:
    typedef enum
    {
        NEW = 0,
        RUNNING,
        EXITED
    } ThreadStatus;
    typedef void (*func_t)(void*);
private:
     pthread_t _tid;
     std::string _name;
     void*_args;
     func_t _func;     //线程未来要执行的回调
     ThreadStatus _status;
};

First of all, we write out the internal content of the thread. The thread needs to have the state of the thread, and the function pointer to complete the callback function, as well as the thread id, name, variable parameter list, etc. Our design of the function pointer is completely consistent with that in the library. In the same way, let's write out the required functions below:

Thread(int num, func_t func, void* args)
     :_tid(0)
     ,_status(NEW)
     ,_func(func)
     ,_args(args)
    {
        char name[128];
        snprintf(name,sizeof(name),"thread-%d",num);
        _name = name;
    }

For the internal initialization of the thread, we directly initialize the thread id to 0 (note that once we create a new thread, the id of the new thread will be returned to tid), the state is new, and then pass the external function pointer and variable parameter list, The printing of the thread name is done in the function body.

int status() 
    { 
        return _status;
    }
    std::string threadname()
    {
        return _name;
    }

Both thread status and thread name can be directly returned to the user. Next, let's implement the run interface:

void run()
    {
        int n = pthread_create(&_tid,nullptr,runHelper,this);
        if (n!=0)
        {
            exit(1);
        }
        _status = RUNNING;
    }

The run interface is to create a thread. The parameter here is the tid inside the thread. After the creation is successful, the tid will become the id of the new thread. If it is not created successfully, it will exit and let it execute the run function. To execute the run function, we need to pass our Thread object, because the following run function is a static type and cannot access private members in the class. We also need to set the status to running.

 static void *runHelper(void* args)   //static后无this指针,满足create接口的第三个参数的要求
    {
        Thread* ts = (Thread*)args;
        (*ts)();
        return nullptr;
    }
    void operator ()()
    {
        _func(_args);
    }

 In order to complete the callback work, the run function must first be a static function, because the member function in the class will have one more parameter by default, this parameter is the this pointer, and our callback function has only one parameter that is void*, so use static, and then we will args Forced to thread*, the following implements a func function, the func function can directly call the func function, so our thread object uses () to call the func function.

void join()
    {
        int n = pthread_join(_tid,nullptr);
        if (n!=0)
        {
            std::cerr<<"main thread join thread"<<_name<<"error"<<std::endl;
            return ;
        }
        _status = EXITED;
    }

Waiting for the thread is also very simple. If the wait is unsuccessful, the error code will be printed and the status will be changed to the exit status.

pthread_t threadid()
    {
        if (_status==RUNNING)
        {
            return _tid;
        }
        else 
        {
            std::cout<<"thread is not running,no tid"<<std::endl;
            return 0;
        }
    }

Before returning the thread id, it is necessary to judge whether the thread is in the running state. Only in the running state will we return its id value, otherwise an error will be printed.

Let's test our thread:

#include "mythread.hpp"
#include <unistd.h>
using namespace std;

void threadRun(void* args)
{
    std::string message = static_cast<const char*>(args);
    while (true)
    {
        cout<<"我是一个线程,"<<message<<endl;
        sleep(1);
    }
}
int main()
{
    Thread t1(1,threadRun,(void*)"hello world");
    cout<<"thread name: "<<t1.threadname()<<"thread id:"<<t1.threadid()<<"thread status: "<<t1.status()<<endl;
    t1.run();
    cout<<"thread name: "<<t1.threadname()<<"thread id:"<<t1.threadid()<<"thread status: "<<t1.status()<<endl;
    t1.join();
    cout<<"thread name: "<<t1.threadname()<<"thread id:"<<t1.threadid()<<"thread status: "<<t1.status()<<endl;
    return 0;
}

 By running we can see that there is no problem with the encapsulated thread. Let's encapsulate the lock ourselves and try it with our own thread

2. Encapsulation of the demo version of the lock

#include <iostream>
#include <pthread.h>

class Mutex    //自己不维护锁,有外部传入
{
public:
    Mutex(pthread_mutex_t *mutex)
       :_pmutex(mutex)
    {

    }
    void lock()
    {
        pthread_mutex_lock(_pmutex);
    }
    void unlock()
    {
        pthread_mutex_unlock(_pmutex);
    }
    ~Mutex()
    {

    }
private:
    pthread_mutex_t *_pmutex;
};

The encapsulation of the lock is very simple. First, there is a lock pointer. When initializing, the external lock is passed to our pointer, and then the lock and unlock operation is performed through this pointer.

class LockGuard   //自己不维护锁,由外部传入
{
public:
    LockGuard(pthread_mutex_t *mutex)
       :_mutex(mutex)
    {
        _mutex.lock();
    }
    ~LockGuard()
    {
        _mutex.unlock();
    }
private:
    Mutex _mutex;
};

Then we are using a class with a lock object in it. When this object is created, it will be automatically locked, and when it is destroyed, it will be automatically unlocked. Let's demonstrate it below:

int main()
{
    Thread t1(1, threadRoutine, (void*)"hello world1");
    Thread t2(2, threadRoutine,(void*)"hello world2");
    Thread t3(3, threadRoutine,(void*)"hello world3");
    Thread t4(4, threadRoutine,(void*)"hello world4");
    t1.run();
    t2.run();
    t3.run();
    t4.run();

    t1.join();
    t2.join();
    t3.join();
    t4.join();
    return 0;
}
int tickets = 1000; 
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
void *threadRoutine(void* args)
{
    std::string message = static_cast<const char*>(args);
    while (true)
    {
        pthread_mutex_lock(&mutex); //所有线程都要遵守这个规则
        if (tickets>0)
        {
            usleep(2000);  //模拟抢票花费的时间
            cout<<message<<" get a ticket: "<<tickets--<<endl;
            pthread_mutex_unlock(&mutex);
        }
        else 
        {
            pthread_mutex_unlock(&mutex);
            break;
        }
    }
    return nullptr;
}

 There is no problem with the ticket grabbing logic after running. Next, we introduce our own encapsulated lock:

 As long as our own lock is in this scope, it is in the locked state, and it will be destroyed when it goes out of scope. It is very convenient to use:

 After running, it is exactly the same as the lock in Curry just now.


Summarize

Reentrant VS thread safe :
Thread safety: When multiple threads concurrently execute the same piece of code, different results will not appear. This problem occurs when global variables or static variables are commonly operated and there is no lock protection.
Reentrancy: The same function is called by different execution flows. Before the current process is executed, other execution flows enter again. We call it reentrance. A function is called a reentrant function if there is no difference in the running result or any problems in the case of reentrancy, otherwise, it is a non-reentrant function.
Common thread unsafe situations :
Functions that do not protect shared variables
A function whose state changes as it is called
function returning a pointer to a static variable
Functions that call thread-unsafe functions
Common thread-safe situations:
Each thread has only read access to global variables or static variables, but no write access. Generally speaking, these threads are safe classes or interfaces are atomic operations for threads.
Switching between multiple threads will not cause ambiguity in the execution results of this interface
Common non-reentrant situations :
The malloc/free function is called , because the malloc function uses a global linked list to manage the heap
Calls to standard I/O library functions, many implementations of the standard I/O library use global data structures in a non-reentrant manner
Static data structures are used in reentrant function bodies
Common reentrant situations:
Do not use global or static variables
Do not use the space opened up by malloc or new
Non-reentrant functions are not called
Does not return static or global data, all data is provided by the caller of the function
Use local data, or protect global data by making a local copy of global data
Reentrancy is associated with thread safety:
Functions are reentrant, that is thread safe
The function is not reentrant, so it cannot be used by multiple threads, which may cause thread safety issues
If a function has global variables, then the function is neither thread-safe nor reentrant.
The difference between reentrant and thread safe:
A reentrant function is a type of thread-safe function
Thread safety is not necessarily reentrant, but reentrant functions must be thread safe.
If the access to the critical resource is locked, the function is thread-safe, but if the reentrant function has not released the lock, it will cause a deadlock, so it is not reentrant

Guess you like

Origin blog.csdn.net/Sxy_wspsby/article/details/130906125