Linux学习之路 -- 线程 -- 互斥

目录

1、概念引入

2、互斥锁

[1、pthread_mutex_init && pthread_ mutex_destory](#1、pthread_mutex_init && pthread_ mutex_destory)

[2、pthread_mutex_lock && pthread_mutex_unlock](#2、pthread_mutex_lock && pthread_mutex_unlock)

3、互斥锁原理的简单介绍


1、概念引入

为了介绍线程的同步与互斥,我们以抢票逻辑引入相关的概念。

示例代码:

复制代码
#include<iostream>
#include<vector>

int g_ticket = 1000;


void* funtion(void* arg)
{
    while(1)
    {
        if(g_ticket > 0)
        {
            //模拟抢票逻辑
            std::cout << "g_ticket : " << g_ticket << " &g_ticket : " << &g_ticket << std::endl;
            g_ticket--;
        }
        else
        {
            break;
        }
    }
    return nullptr;
}
const int num = 5;
int main()
{
    std::vector<pthread_t> pthread;
    for(int i = 0; i < num; i++)
    {
        pthread_t id;
        pthread_create(&id,nullptr,funtion, nullptr);
        pthread.emplace_back(id);
    }
    for(auto& e: pthread)
    {
        pthread_join(e,nullptr);
    }
    return 0;
}

我们创建多个线程,然后让不同的线程对同一个变量进行操作。也就是让不同线程访问同一个函数,抢票函数会让票数减减。我们运行一下代码,观察一下结果。

这里我们可以看见,g_ticket的地址是一样的,说明访问的变量一定是同一全局变量。而g_ticket的数值却出现-2,-3。这明显是不符合我们的要求的,所以这里一定是有问题的,而这也叫数据不一致 。

首先解释一下为什么会造成这种现象,首先if中的条件判断是逻辑判断,这是要在cpu内进行的运算的。(我们假设g_ticket的值为1)

每当一个线程在将内存中g_ticket数据放入cpu中的寄存器,由cpu执行判断逻辑运算。在判断成功后,该线程就有可能直接被切走了,没有执行到下面的g_ticket--操作,而内存中的g_ticket还是1。下个线程也会重复上述的操作,这就导致虽然只有一张票,但却还是有多个线程进入该函数中。线程同时执行了ticket--操作,所以就会造成ticket被减到负数的情况。同时,需要注意的是,ticket--也不是原子的,这段代码在cpu中的执行过程分为三步,第一步是将内存中g_ticket读取到cpu中,第二步是将cpu中的g_ticket做减减操作,第三步是将cpu中的值写回内存中。这里的每一步在结束后,线程可能都会被切走,这也会造成g_ticket这个数据不安全,不过这里一般出错概率较低,主要还是上述因素造成的。这种因为原子性问题导致数据不一致情况还是比较常见的。(原子性:只有执行和没执行两种状态,说通俗一点就是翻译成汇编只有一条语句,上面的++操作翻译成汇编就有3条语句) 总结一下,这里g_ticket出现负数情况的原因,其实就是这个共享资源没有被保护,并且访问该共享资源的过程并不是原子性的。

2、互斥锁

如何解决上述的问题呢?这里我们就需要引入锁的概念了。原生线程库不仅提供了线程创建等相关的接口,还提供了互斥锁的相关接口。我们在线程中常用的锁一般称为互斥锁,互斥的概念前面已经有所介绍,这里不再赘述。我们只要对共享资源进行加锁,就能防止出现上述的问题。下面先介绍一下相关的接口。

1、pthread_mutex_init && pthread_ mutex_destory

在使用锁之前,我们首先需要定义一个pthread_mutex_t类型的变量。如果这个变量是局部变量,我们就需要使用pthread_mutex_init进行初始化(这里的第二参数表示锁的属性,这里定义成nullptr即可),同时在使用完后,要对锁进行pthread_mutex_destory释放。如果这个变量是全局变量,则我们只需要在定义变量时,让其等于PTHREAD_MUTEX_INITALIZER进行初始化即可,后面不需要手动进行销毁。

2、pthread_mutex_lock && pthread_mutex_unlock

当我们初始化锁以后,我们就需要使用锁了,pthread_mutex_lock 就表示申请上锁,申请成功,函数返回,继续向后执行;申请失败,一直阻塞直至申请成功;如果函数调用失败,出错返回。而trylock接口在申请失败后会直接返回,这是和lock接口的区别。而unlock就表示解开该锁。

下面演示一下加锁例子,我们以上面抢票逻辑的代码为例子。

ThreadMode.hpp

复制代码
#ifndef __THREAD_HPP__
#define __THREAD_HPP__

#include <iostream>
#include <string>
#include <unistd.h>
#include <functional>
#include <pthread.h>

namespace ThreadModule
{
    template<typename T>
    using func_t = std::function<void(T)>;
    // typedef std::function<void(const T&)> func_t;

    template<typename T>
    class Thread
    {
    public:
        void Excute()
        {
            _func(_data);
        }
    public:
        Thread(func_t<T> func, T data, const std::string &name="none-name")//右值
            : _func(func), _data(data), _threadname(name), _stop(true)
        {}
        static void *threadroutine(void *args) // 类成员函数,形参是有this指针的!!
        {
            Thread<T> *self = static_cast<Thread<T> *>(args);
            self->Excute();
            return nullptr;
        }
        bool Start()
        {
            int n = pthread_create(&_tid, nullptr, threadroutine, this);
            if(!n)
            {
                _stop = false;
                return true;
            }
            else
            {
                return false;
            }
        }
        void Detach()
        {
            if(!_stop)
            {
                pthread_detach(_tid);
            }
        }
        void Join()
        {
            if(!_stop)
            {
                pthread_join(_tid, nullptr);
            }
        }
        std::string name()
        {
            return _threadname;
        }
        void Stop()
        {
            _stop = true;
        }
        T& Data()
        {
            return _data;
        }
        ~Thread() {}

    private:
        pthread_t _tid;
        std::string _threadname;
        T _data;  // 为了让所有的线程访问同一个全局变量
        func_t<T> _func;
        bool _stop;
    };
} // namespace ThreadModule

#endif

#include <iostream>
#include "ThreadMode.hpp"
#include <vector>

int g_ticket = 1000;
using namespace ThreadModule;
template <class T>
class ThreadData
{
public:
    ThreadData(int &data, const std::string str) : _data(data), name(str), total(0)
    {
    }
    ~ThreadData()
    {
    }
    std::string Getname()
    {
        return name;
    }
    void buyticket()
    {
        _data--;
    }
    int Geticket()
    {
        return _data;
    }
    void Plus()
    {
        total++;
    }
    void Total()
    {
        std::cout << name << " : " << total << std::endl;
    }

private:
    int &_data;
    std::string name;
    int total;
};
pthread_mutex_t _lock = PTHREAD_MUTEX_INITIALIZER;//全局锁
void funtion(ThreadData<int> *td)
{
    while (1)
    {
        //加锁
        pthread_mutex_lock(&_lock);
        if (g_ticket > 0)
        {
            std::cout << td->Getname() << " get ticket, remain ticket number: " << td->Geticket() << std::endl;
            td->buyticket();
            pthread_mutex_unlock(&_lock);
            td->Plus();
        }
        else
        {
            pthread_mutex_unlock(&_lock);
            break;
        }
    }
}
const int num = 5;
int main()
{
    std::vector<Thread<ThreadData<int> *>> thread;
    for (int i = 0; i < num; i++)
    {
        // char* threadname = new char[64];
        // snprintf(threadname, 64, "Thread-%d", i + 1);
        std::string threadname = "thread -" + std::to_string(i + 1);
        ThreadData<int> *ptr = new ThreadData<int>(g_ticket, threadname);
        thread.emplace_back(Thread<ThreadData<int> *>(funtion, ptr, threadname));
    }
    for (auto &e : thread)
    {
        e.Start();
    }
    for (auto &e : thread)
    {
        sleep(1);
        e.Data()->Total();
        e.Join();
        delete e.Data();
    }
    return 0;

}

这里我们着重看funtion执行函数即可,在该函数外部,我定义了一把全局锁。当不同线程执行同一函数,需要访问一块共享资源(临界资源)的代码,我们就称为临界区,其它部分,我们称为非临界区。而我们要保护的,其实就是临界区的资源,这就要求我们在加锁时,尽量只要对临界区的部分进行加锁即可,对其他非临界区的部分,可以不用管。这里当多个线程同时访问同一变量时,就需要去竞争那一把全局锁。谁竞争到了锁,谁就能对临界资源进行访问,其余线程只能在临界区外进行等待。当然,这里不排除有的线程竞争锁的能力很强,让其他线程根本就竞争不到锁的情况,这就会造成其他进程的饥饿问题。在不同系统下,不同线程的竞争能力不同,这和锁的创建时间、os的调度算法等有管。

运行结果

这里就出现了线程4、5的竞争问题,不过这里,我们暂不探究。而票数这个全局变量,在加锁后,就回复正常了。在C++中,对互斥锁的释放和初始化等等操作进行了包装,这里不一一介绍,如果有需要,也可以自行封装。

3、互斥锁原理的简单介绍

互斥锁底层在不同OS下可能有不同的实现方式,这里简单介绍一种。在互斥锁中,实际上表示持有锁的状态就是用一个整数,我们可以用+或-来改变其状态,但我们前面提到,++操作并不是原子性的,所以这个用整数来表示持有锁的状态是有一定问题的。为了解决该问题,系统中有特定的汇编指令,能够原子地交换cpu寄存器和物理内存中的数值。我们用1表示持有锁,0表示不持有锁。

假设我们现在定义了一把锁,我们lock这个整型变量来表示锁地使用情况。如果lock为0,表示锁被使用,不为零,就被没使用。在内存中的数据大部分是被线程共享的,而在cpu上的寄存器中存储的硬件上下文是被线程私有的。

首先,线程内部也有整型变量表示是否持有锁,我们以0表示不持有锁,1表示持有锁。以上图为例,thread-1申请锁时,会首先将线程内部的变量读入寄存器中,然后通过特殊汇编指令与内存中的lock值交换(该过程为原子的),此时就完成了锁的申请。此时时间片耗尽,线程就会切换,同时寄存器中的硬件上下文也会被清空,锁被带走。第二个线程也会将自己表示持有锁状态的变量读入寄存器中,然后重复上述的动作,但是由于lock变量已经为零,所以第二个线程即使交换完后,也是无法持有锁的。解锁,就是把线程上的数据交换会内存中,表现在图上就是线程中的"1"换回内存中lock变量。所以其他线程想要访问临界资源,就只能等待线程把锁释放,访问临界区的过程也就是线程安全的。

以上就是所有的内容,文中如有不对之处,还望各位大佬指正,谢谢!!!

相关推荐
code_pgf10 分钟前
`unordered_map` 详解
c++
零涂毕业设计19 分钟前
毕业设计模块开发-OLED显示屏(IIC协议0.96寸)STM32 ESP32 FPGA Linux驱动免费分享
linux·stm32·单片机·嵌入式·esp32·fpga·oled
tedcloud12330 分钟前
OmniRoute怎么部署?开源AI模型路由平台Linux部署教程
linux·服务器·人工智能·开源·音视频
SakitamaX1 小时前
LVS(Linux Virtual Server)概念详解
linux·运维·lvs
小张同学a.1 小时前
Linux系统管理
linux·运维
阿龙的工作记录1 小时前
深入理解Linux设备驱动模型
linux·嵌入式硬件·工作记录
代码村新手2 小时前
Linux的基本指令
linux·运维·服务器
一拳一个呆瓜2 小时前
【STL】iostream 编程:检测提取操作产生的错误
c++·stl
稚南城才子,乌衣巷风流2 小时前
判断素数与拓展应用
c++
众少成多积小致巨2 小时前
C++ 规范参考(下)
c++