【Linux】互斥量原理的实现

深刻理解互斥锁

文章目录

  • 前言
  • 一、demo版的线程封装
  • 二、demo版的锁封装
  • 总结


前言

为了实现互斥锁操作 , 大多数体系结构都提供了 swap exchange 指令 , 该指令的作用是把寄存器和内存单元的数据相交换, 由于只有一条指令 , 保证了原子性 , 即使是多处理器平台 , 访问内存的总线周期也有先后 , 一个处理器上的交换指令执行时另一个处理器的交换指令只能等待总线周期。如下图:
【Linux】互斥量原理的实现_第1张图片

 【Linux】互斥量原理的实现_第2张图片

 对于上图中的加锁解锁汇编代码,是谁在执行呢?答案是调用的线程。

【Linux】互斥量原理的实现_第3张图片

【Linux】互斥量原理的实现_第4张图片

 这里圈出来的汇编代码的意思是:将共享数据交换到自己的私有上下文当中。这是什么意思呢下面我们详细的讲解一下:

首先我们的线程进来后加锁然后向自己的上下文写入0就是上面图中这一局代码,在这里要记住未来我们切换线程的时候会将这个上下文中的0带走的,因为我们定义的锁在内存中存放,既然是内存那么注定了这个锁是共享的。然后我们执行下一条指令就是倒数第二张图中红色部分,这里就是将寄存器里的值和mutex变量里的内容做交换,以前我们的寄存器是0内存是1,现在变成了1,0如下图:

【Linux】互斥量原理的实现_第5张图片

 这也就证明了我们刚刚说的将共享数据交换到自己的私有上下文当中 ,这就是加锁的原理。

这个时候进入if语句,因为我们刚刚和寄存器做了交换,所以这次直接加锁成功返回0.在这里我们提一句,如果还没进入if判断语句线程就被切换了会怎么样呢?这个时候第一个线程会带走自己的上下文,也就是说寄存器里的1没有了被线程带走了,如下图:

【Linux】互斥量原理的实现_第6张图片

 这个时候第二个线程进来了,第二个进程也要申请锁,所以先和内存中的mutex做交换,因为刚刚mutex的1已经被第一个线程拿走了,所以交换完寄存器和mutex还是0,这个时候进入if判断语句发现不大于0只能挂起等待,后续来申请锁的线程都是如此,因为1(钥匙)已经被第一个线程拿走了!!这个时候操作系统将第一个线程拿过来,然后发现寄存器中的内容1大于0然后就申请锁成功了。所以上面的mutex的1只能进行流转,不会新增任何的1。解锁也很简单,直接将执行流中的mutex改为1即可,因为解锁只有一条指令,所以相当于原子性的解锁了。 


一、demo版的线程封装

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;
};

首先我们把线程内部的内容写出来,线程中需要有线程的状态,和函数指针完成回调函数,以及线程的id,名称,可变参数列表等,我们对于函数指针的设计完全是和库里面的一样的,下面我们把需要的函数写出来:

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;
    }

对于线程内部的初始化我们直接将线程id初始化为0(注意一旦我们创建新线程后新线程的id会返回给tid),状态为new,然后将外面的函数指针传过来还有可变参数列表,在函数体内完成线程名称的打印。

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

线程状态和线程名称都可以直接返回给用户,下面我们实现一下run接口:

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

run接口就是创建线程了,我们这里的参数就是线程内部的tid,创建成功后tid会变成新线程的id,如果没有创建成功就退出,让其执行run函数,执行run函数还需要传我们的线程对象,因为下面的run函数是static类型无法访问类内私有成员。我们还需要将状态设为运行。

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

 run函数为了完成回调工作首先必须是static函数,因为类内成员函数会默认多一个参数,这个参数是this指针,而我们的回调函数只有一个参数是void*的,所以用static,然后我们将args强转为thread*,下面实现一个仿函数,仿函数是可以直接调用func函数,所以我们的线程对象使用()就会调用func函数。

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

等待线程也很简单,如果等待不成功就打印错误码,将状态改为退出状态。

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

返回线程id前需要先判断该线程是否是运行状态,只有运行状态我们才返回其id值,否则就打印错误。

下面我们测试一下我们的线程:

#include "mythread.hpp"
#include 
using namespace std;

void threadRun(void* args)
{
    std::string message = static_cast(args);
    while (true)
    {
        cout<<"我是一个线程,"<

【Linux】互斥量原理的实现_第7张图片

 通过运行我们可以看到封装的线程并没有问题,下面我们把锁也自己封装一下然后用我们自己的的线程试一下

二、demo版的锁的封装

#include 
#include 

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;
};

对于锁的封装就非常简单了,首先有一个锁的指针,初始化的时候把外部那个锁传给我们的指针,然后通过这个指针去进行加锁解锁操作。

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

然后我们在用一个类,里面有一个锁对象,当这个对象创建的时候会自动加锁,销毁的时候自动解锁,下面我们演示一下:

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(args);
    while (true)
    {
        pthread_mutex_lock(&mutex); //所有线程都要遵守这个规则
        if (tickets>0)
        {
            usleep(2000);  //模拟抢票花费的时间
            cout<

【Linux】互斥量原理的实现_第8张图片

 运行后抢票逻辑也是没有问题的,下面我们引入我们自己封装的锁:

【Linux】互斥量原理的实现_第9张图片

 我们自己的锁只要在这个作用域就是加锁状态,出了作用域就销毁了用起来非常的方便:

【Linux】互斥量原理的实现_第10张图片

 运行后和刚刚库里的锁一模一样。


总结

可重入 VS 线程安全
线程安全:多个线程并发同一段代码时,不会出现不同的结果。常见对全局变量或者静态变量进行操作,并且没有锁保护的情况下,会出现该问题。
重入:同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,我们称之为重入。一个函数在重入的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为可重入函数,否则,是不可重入函数。
常见的线程不安全的情况
不保护共享变量的函数
函数状态随着被调用,状态发生变化的函数
返回指向静态变量指针的函数
调用线程不安全函数的函数
常见的线程安全的情况:
每个线程对全局变量或者静态变量只有读取的权限,而没有写入的权限,一般来说这些线程是安全的类或者接口对于线程来说都是原子操作
多个线程之间的切换不会导致该接口的执行结果存在二义性
常见不可重入的情况
调用了 malloc/free 函数,因为 malloc 函数是用全局链表来管理堆的
调用了标准 I/O 库函数,标准 I/O 库的很多实现都以不可重入的方式使用全局数据结构
可重入函数体内使用了静态的数据结构
常见可重入的情况:
不使用全局变量或静态变量
不使用用 malloc 或者 new 开辟出的空间
不调用不可重入函数
不返回静态或全局数据,所有数据都有函数的调用者提供
使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据
可重入与线程安全联系:
函数是可重入的,那就是线程安全的
函数是不可重入的,那就不能由多个线程使用,有可能引发线程安全问题
如果一个函数中有全局变量,那么这个函数既不是线程安全也不是可重入的。
可重入与线程安全区别:
可重入函数是线程安全函数的一种
线程安全不一定是可重入的,而可重入函数则一定是线程安全的。
如果将对临界资源的访问加上锁,则这个函数是线程安全的,但如果这个重入函数若锁还未释放则会产生死锁,因此是不可重入的

你可能感兴趣的:(linux,linux,后端,c++,vscode,运维)