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变量。所以其他线程想要访问临界资源,就只能等待线程把锁释放,访问临界区的过程也就是线程安全的。

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

相关推荐
蜀黍@猿8 分钟前
【C++ 基础】从C到C++有哪些变化
c++
Am心若依旧40910 分钟前
[c++11(二)]Lambda表达式和Function包装器及bind函数
开发语言·c++
真真-真真11 分钟前
WebXR
linux·运维·服务器
zh路西法20 分钟前
【C++决策和状态管理】从状态模式,有限状态机,行为树到决策树(一):从电梯出发的状态模式State Pattern
c++·决策树·状态模式
轩辰~34 分钟前
网络协议入门
linux·服务器·开发语言·网络·arm开发·c++·网络协议
lxyzcm1 小时前
C++23新特性解析:[[assume]]属性
java·c++·spring boot·c++23
蜀黍@猿1 小时前
C/C++基础错题归纳
c++
虾球xz1 小时前
游戏引擎学习第55天
学习·游戏引擎
雨中rain1 小时前
Linux -- 从抢票逻辑理解线程互斥
linux·运维·c++
oneouto1 小时前
selenium学习笔记(二)
笔记·学习·selenium