• 【1++的Linux】之线程(二)


    👍作者主页:进击的1++
    🤩 专栏链接:【1++的Linux】

    一,对上一篇内容的补充

    线程创建: pthread_ create函数第一个参数指向一个虚拟内存单元,该内存单元的地址即为新创建线程的线程ID,属于NPTL线程库的范畴。线程库的后续操作,就是根据该线程ID来操作线程的。线程库NPTL提供了pthread_ self函数,可以获得线程自身的ID

    **线程终止:**需要注意,pthread_exit或者return返回的指针所指向的内存单元必须是全局的或者是用malloc分配的,不能在线程函数的栈上分配,因为当其它线程得到这个返回指针时线程函数已经退出了。

    线程等待: 已经退出的线程,其空间没有被释放,仍然在进程的地址空间内。创建新的线程不会复用刚才退出线程的地址空间。(线程等待的原因)

    int pthread_join(pthread_t thread, void **value_ptr);

    1. 如果thread线程通过return返回,value_ ptr所指向的单元里存放的是thread线程函数的返回值。
    2. 如果thread线程被别的线程调用pthread_ cancel异常终掉,value_ ptr所指向的单元里存放的是常数
      PTHREAD_ CANCELED。
    3. 如果thread线程是自己调用pthread_exit终止的,value_ptr所指向的单元存放的是传给pthread_exit的参
      数。
    4. 如果对thread线程的终止状态不感兴趣,可以传NULL给value_ ptr参数。

    线程分离: 默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行pthread_join操作,否则无法释放资源,从而造成系统泄漏。如果不关心线程的返回值,join是一种负担,这个时候,我们可以告诉系统,当线程退出时,自动释放线程资源。
    joinable和分离是冲突的,一个线程不能既是joinable又是分离的。

    二,Linux线程互斥

    1. 互斥的引出

    我们再来回顾一下我们以前曾提到过的临界资源,临界区和原子性。

    临界资源:被多个执行流所共享的资源叫做临界资源。
    临界区:每个线程内部执行访问临界资源的代码叫做临界区。
    原子性:在别人看来只有两种状态,做一件事情,要么没做,要么做完。

    首先我们来回答为什么要有线程互斥。

    我们来看一段代码:

    #include
    #include
    #include
    #include
    using namespace std;
    
    int tickets=1000;
    void* get_tickets(void* argv)
    {
        while(true)
        {
            if(tickets>0)
            {
                cout<<"我是:"<<pthread_self()<<" "<<"我抢的第"<<tickets<<"票"<<endl;
                tickets--;
            }
            else
            {
                break;
            }
        }
    
        cout<<"票完了"<<endl;
        return nullptr;
    }
    
    int main()
    {
        pthread_t tid[3];
        for(int i=0;i<3;i++)
        {
            pthread_create(tid+i,nullptr,get_tickets,(void*)"thread");//创建多个线程
        }
    
        //等待线程
        for(int i=0;i<3;i++)
        {
            pthread_join(tid[i],nullptr);
        }
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41

    在这里插入图片描述
    我们看到,最终的结果竟然有两个线程抢到了同一个编号的票,这样岂不是一个作为我们卖出去了两张甚至更多的票。

    这是为什么呢?
    我们用下面这张剖析图来理解:
    在这里插入图片描述

    有如下场景:线程A先执行,票数减减的步骤在我们看到就只有一行,实际转换为汇编代码后是有三条语句,图中我已经写出,当A执行完前两步,要将数据写入内存中去时,因为时间片等某种原因,其被切换了下来,换进程B去执行,此时在内存中票的数仍然为1000,所以B拿到的仍然是编号为1000的票,因此就发生了上述结果。
    因此就要有互斥的存在了!!!

    多线程是共享地址空间的,所以有很多资源都是共享的。
    这种方式带来的优势:方便了线程间的通信
    缺点:并发访问一些共享的数据时,回由于时序问题而导致数据不一致的问题。

    那么什么是互斥呢?
    我们先来看其概念:任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用。
    以上述代码为例,那么我们的临界资源就是票的数量,临界区就为抢票过程的一段代码

    在这里插入图片描述
    互斥就是对临界区的保护的一种方式,其本质就是保护临界资源

    2. 互斥量

    要解决上述数据不一致的问题,需要做到三点:

    1. 代码必须要有互斥行为:当代码进入临界区执行时,不允许其他线程进入该临界区。
    2. 如果多个线程同时要求执行临界区的代码,并且临界区没有线程在执行,那么只能允许一个线程进入该临界区。
    3. 如果线程不在临界区中执行,那么该线程不能阻止其他线程进入临界区。
      要做到这三点,本质上就是需要一把锁。Linux上提供的这把锁叫互斥量

    锁的初始化:
    在这里插入图片描述

    锁的初始化有两种,一种是用函数做初始化,将你的锁的地址传入进去,属性可以设置为nullptr,另一种是对于全局的锁或者是static修饰的锁,可以直接用宏PTHREAD_MUTEX_INITIALIZER 进行初始化。

    锁的销毁:

    pthread_mutex_destroy是销毁锁。
    销毁互斥量需要注意:
    使用 PTHREAD_ MUTEX_ INITIALIZER 初始化的互斥量不需要销毁
    不要销毁一个已经加锁的互斥量
    已经销毁的互斥量,要确保后面不会有线程再尝试加锁

    加锁和解锁:
    在这里插入图片描述

    int pthread_mutex_lock(pthread_mutex_t *mutex);//加锁
    int pthread_mutex_trylock(pthread_mutex_t *mutex);//加锁
    int pthread_mutex_unlock(pthread_mutex_t *mutex);//解锁
    返回值:成功返回0,失败返回错误号。
    调用 pthread_ lock 时:

    1. 互斥量处于未锁状态,该函数会将互斥量锁定,同时返回成功。
    2. 发起函数调用时,其他线程已经锁定互斥量,或者存在其他线程同时申请互斥量,但没有竞争到互斥量,那么pthread_ lock调用会陷入阻塞(执行流被挂起),等待互斥量解锁。
      trylock函数,:
      这个函数是非阻塞调用模式, 也就是说, 如果互斥量没被锁住, trylock函数将把互斥量加锁, 并获得对共享资源的访问权限; 如果互斥量被锁住了, trylock函数将不会阻塞等待而直接返回EBUSY, 表示共享资源处于忙状态。

    有了锁之后我们对抢票系统做出改进:

    #include
    #include
    #include
    #include
    using namespace std;
    
    pthread_mutex_t mt=PTHREAD_ADAPTIVE_MUTEX_INITIALIZER_NP;//创建锁并初始化
    int tickets=1000;
    void* get_tickets(void* argv)
    {
        (void*)argv;
        while(true)
        {
            pthread_mutex_lock(&mt);//加锁
            if(tickets>0)
            {
                cout<<"我是:"<<pthread_self()<<" "<<"我抢的第"<<tickets<<"票"<<endl;
                tickets--;
                pthread_mutex_unlock(&mt);
            }
            else
            {
                 pthread_mutex_unlock(&mt);
                break;
    
            }
        }
    
        cout<<"票完了"<<endl;
        return nullptr;
    }
    
    int main()
    {
        pthread_t tid[3];
        for(int i=0;i<3;i++)
        {
            pthread_create(tid+i,nullptr,get_tickets,(void*)"thread");//创建多个线程
        }
    
        //等待线程
        for(int i=0;i<3;i++)
        {
            pthread_join(tid[i],nullptr);
        }
    
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48

    在这里插入图片描述
    此时我们可以看到运行结果就达到了我们所期望的。

    下面我们换一种锁的初始化方式来进行验证:

    #define TH_NUM 5
    
    class Get_tickets
    {
        public:
        Get_tickets(string& name,pthread_mutex_t* mut)
            :_name(name)
            ,_mut(mut)
            {}
        
        public:
        string _name;
        pthread_mutex_t* _mut;
    
    };
    
    void* get_tickets(void* argv)
    {
        Get_tickets* Lock=(Get_tickets*)argv;
        while(true)
        {
            pthread_mutex_lock(Lock->_mut);
            if(tickets>0)
            {
                cout<<"I am "<<Lock->_name<<": "<<tickets<<endl;
                tickets--;
                pthread_mutex_unlock(Lock->_mut);
                usleep(100000);
            }
            else
            {
                pthread_mutex_unlock(Lock->_mut);
                break;
            }
        }
    
        cout<<"票完了"<<endl;
    }
    int main()
    {
        pthread_t tid[TH_NUM];
        pthread_mutex_t mut;
        pthread_mutex_init(&mut,nullptr);
        string name="thread";
        for(int i=0;i<TH_NUM;i++)
        { 
            Get_tickets* pLock=new Get_tickets(name,&mut);
            pLock->_name+=to_string(i+1);
            pthread_create(tid+i,nullptr,get_tickets,(void*)pLock);
        }
    
        for(int i=0;i<TH_NUM;i++)
        {
            pthread_join(tid[i],nullptr);
        }
    
        return 0;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59

    在这里插入图片描述
    我们发现结果符合我们的预期。

    3. 剖析锁的原理

    由于我们对临界区加了锁,因此多个执行流在访问临界值的时候都是串行的,也就是说每次只让一个执行流区访问临界资源直到出了临界区,也就是解锁后才回再让这些执行流去竞争进入临界区。(对于临界区,我们的临界区要尽量短小精悍,因为锁是回影响执行效率的,这违背了我们创建线程的初衷,因此非必要不适用锁) 我们的临界资源加了锁后我们就可以说它是原子的。我们在访问临界资源时,先访问的是锁,先会去判断是否已经加锁了,并且会有多个执行流看到它,那么锁是不是临界资源呢?或者说是互斥锁是不是原子的呢?锁自己都保护不好自己怎么去保护别人,那么该如何去保证锁的安全呢?

    我们先抛出答案,锁是具有原子性的。

    我们来看看关于加锁和解锁的两段伪代码:
    在这里插入图片描述
    我们再次以一张图来理解这段伪代码:
    在这里插入图片描述
    我们有如下场景:
    A线程正在进行加锁的过程,我们可以把申请的这把锁中的内容 “1” 看作一个令牌(一个锁只有一个)先是将0写入特定的寄存器当中,接着将锁的内容和寄存器中的0进行交换(这一步在汇编中只有一行代码,因此这一操作也是原子的) 若,这是,A线程被切换掉,B线程执行,此时会在从第一步开始执行,将0写入,然后交换,但这是交换到寄存器中的值是A进行交换时交换过去的0,因此在判断是,其会被挂起等待,此时A线程被换上去继续执行,恢复其上下文数据后,(这段数据中,也会记录A上次执行到了那一步),此时寄存器中的值就为恢复上来的1,进行判断后,加锁成功。
    这就好比上面所提到的只有一个令牌,只要执行完交换语句后,A就拿到了这个令牌,成为它上下文中的一部分,哪怕被切下去,也没有关系,因为,寄存器只有一份,但寄存器中的数据可以有很多分。寄存器中的内容,是每一个执行流私有的。
    此时B虽然被调度执行,但令牌已经没了,所以B只能等待。

    对于解锁,就是将令牌归还与锁,这一动作也是有原子的。归还后,等待的线程会再次重复上述争令牌的过程。

    交换的现象:内存<---->寄存器
    交换的本质:原本锁中的数据:共享---->私有

  • 相关阅读:
    【学习日记2023.5.23】 之 Redis入门未入坑
    ElasticSearch之score打分机制原理
    【毕业设计】基于SSM与VUE的在线医疗诊断跟踪系统
    使用MyBatis框架进行关系型数据库操作
    Lambda总结
    Redis基础概念
    Nuxt3区分环境打包报错“flase.appcet is not function“
    MVC模式简介
    圈重点|等保2.0灾备方案怎么做?
    代理IP和Socks5代理:跨界电商与全球爬虫的关键技术
  • 原文地址:https://blog.csdn.net/m0_63135219/article/details/134252672