Deep understanding of mutex
Article Directory
foreword
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.