【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<iostream>
#include<pthread.h>
#include<unistd.h>
#include<string>
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;
}

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

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

有如下场景:线程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<iostream>
#include<pthread.h>
#include<unistd.h>
#include<string>
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;
}

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

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

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

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

3. 剖析锁的原理

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

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

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

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

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

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

进击的1++

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值