linux入门---线程的同步

目录标题

  • 什么是同步
  • 生产者和消费者模型
  • 三者之间的关系
  • 消费者生产者模型改进
  • 生产者消费者模型特点
  • 条件变量的作用
  • 条件变量有关的函数
  • 条件变量的理解
  • 条件变量的使用

什么是同步

这里通过一个例子来带着大家了解一下什么是同步,在生活中大家肯定遇到过排队的情景比如说某个小吃店在做活动然后很多人都在排队,然后小王恰巧路过这个小吃店,小王知道这个小吃店特别的火里面的东西买的很贵但是非常好吃,碰巧今天正在做活动并且优惠的力度特别大,于是小王就开始来到队队伍的最后排起队来准备买点小吃回家,小王边跟微信好友聊天炫耀自己抢到优惠的同时排在小王后面的人越来越多并且小王也越来越靠近售卖的地方,过了一会终于轮到了小王,小王买了一份小吃然后付完钱准备离开的时候小王突然想起来自己的妈妈也很喜欢这个小吃然后小王看到队伍后面有好多的人不想重修排队,于是小王又抢在下一个人上前购买之前来到收银台再买一份,等小王又付完钱刚离开的时候小王看到消息发现一号好兄弟让他帮忙带一份小吃于是他就又抢走下一个人来到收银台之前跑到收银台旁边进行挑选,等小王付完钱刚准备离开的时候他又发现自己的二号好兄弟也找他代购小吃于是他又赶在下一个来到收银台之前跑到收银台进行挑选购买,就这样反反复复然后就会发现一个现象这条有很多人的队伍停止运转了,因为小王是离收银台最近的人,所以小王每次都能赶在下一个人之前来到收银台进行下一次购买,这就导致了只有小王一个人能买到小吃抢到资源,这是不公平的也不是小吃店所期望,小吃店之所以做这么大的活动是希望通过这个活动让更多的人都能品尝到这份美味而不是只让小王一个人买到这份美味,那么这就是生活中的例子,在程序的代码中这样的现象依然存在,比如说下面的代码:

#include
#include
#include
#include
using namespace std;
int ticket_num=1000;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
void * func(void* args)
{
    string name=static_cast<const char*>(args);
    int sum=0;
    while(true)
    {
        pthread_mutex_lock(&mutex);
        // LockGuard lockguard(&mutex);
        if(ticket_num>0)
        {
            usleep(124);
            cout<<name<<" 正在进行抢票 "<< ticket_num<<endl;
            --ticket_num;
            sum++;
            pthread_mutex_unlock(&mutex);
        }
        else
        {
            pthread_mutex_unlock(&mutex);
            break;
        }
    }
    cout<<name<<"抢到票的个数:"<<sum<<endl;
    return nullptr;
}
int main()
{
    pthread_t tid1;
    pthread_t tid2;
    pthread_t tid3;
    pthread_create(&tid1,nullptr,func,(void*)"user1");
    pthread_create(&tid2,nullptr,func,(void*)"user2");
    pthread_create(&tid3,nullptr,func,(void*)"user3");
    pthread_join(tid1,nullptr);
    pthread_join(tid2,nullptr);
    pthread_join(tid3,nullptr);
    return 0;
}

这是一个抢票的程序一共有1000张票我们创建了3个线程互斥的欠票,如果资源的分配是公平的话我们应该可以看到每个线程抢到票的个数应该是相差不大的,那么将程序运行一下就可以看到下面这样的结果:
linux入门---线程的同步_第1张图片
我们发现user3抢到了绝大部分的票,user1只抢到了182张票,而user3却1张票都没有抢到,那么这就说明上面锁资源的分配是不公平的,某些线程可以容易的申请锁资源而有些线程却很难申请到锁的资源,申请到了锁资源就要执行一些任务完成事情,如果某个线程一直申请到了锁的资源那么这个线程就一直处于工作状态而一直没有申请到锁资源的程序就一直处于空闲状态,也就是所谓的忙的忙死闲的闲死,那为什么会出现这样的现象呢?原因很简单锁只规定了互斥访问也就是让执行流串行的访问某个区域,他并没有规定让谁先优先获取锁所以哪个线程能获取到锁完全由线程之间的竞争决定的,出现了这样的现象是不符合预期的我们希望每个线程都能够均匀的分配到工作,所以这里就采用一个方法,当我们排队购买东西的时一旦付完了钱要是想再次购买的话只能重新排队来到队伍的最后不能直接插到最前面,那么这里也是同样的道理,我们让线程能够按照某种特定的顺序访问临界资源也就是按照某种顺序申请锁资源,比如说先是A线程申请锁再是B线程申请锁然后是c线程申请锁等绕了一圈之后才又是A线程申请锁
linux入门---线程的同步_第2张图片

这样就避免了饥饿问题,每个线程都能分配到数量相当的任务我们把这样的解决方法叫做同步。

生产者和消费者模型

那么要想更好的了解线程的同步我们就得了解一下什么是生产者消费者模型,在现实中学生就是典型的消费者,学生一般会去商场消费买东西,那么生产者就是提供商品的工厂,学生不会去工厂里面买东西,而是去超市里面买东西,因为工厂不会卖东西给消费者因为机器的启动成本比较高学生购买的量比较少,并且工厂离学生等消费群体比较远不会建立在市区,所以消费者会去超市里面买东西,超市就是一个平台他建立在市区离学生群体较近,面向消费者提供商量货物售卖的服务,面向工厂提供大量购买的请求

linux入门---线程的同步_第3张图片

超市里面不同的商品就有这不同的供货商,并且超市面向的消费者比较多,所以超市每次向供货商进货的量就比较大,所以超市的作用就是集中需求分发产品,所以超市扮演的角色就是交易场所,生产者并不是无时无刻在生产,可能在修理设备可能在停工也可能在生产产品,并且消费者也不是无时无刻在消费他们可能在工作可能在上学也可能在干任何事情,所以消费者和生产者之间是没有什么关系的他们两做的事情是不会发生干扰的,我们把这样的现象称为生产的过程和消费的过程解藕,那这里就有两个问题:消费者能够一直消费吗?消费者不能一直消费当把超市里面的东西买完了也就没得买了,想消费也消费不了了所以消费者不能够一直消费,那生产者能够一直生产吗?肯定也是不行的商场都货架都摆满了你还生产干嘛咧对吧卖不出去就不生产了对吧,所以生产者也不能一直生产,超市作为临时保存产品的场所他可以保存一定量的产品,所以他就可以保证生产者要生产的时候可以一定程度的生产,生产者不想生产的时候我这里也有货可以一定程度满足消费者的需求,而消费想消费的时候超市目前的存货量也能几乎满足他的需求,所以正式超市这样的角色存在就保证了生产者和消费者在一定程度上的解耦,而在计算机中我们就把超市这样的角色称为缓冲区。这里为了让大家更好的了解什么是解耦我们可以举一个强耦合的例子:函数调用就是强耦合,我们以形参的形式将数据交给函数func,函数体内对新参进行加工将计算的结果以返回值的方式进行返回,所以可以将函数的调用方看成生产者生产了数据,把形成的变量认为临时的保存了数据,而目标函数就是消费者他拿着临时数据开始了加工和消费,我们知道在main函数里面调用了func函数,那在执行func函数的时候main函数在干嘛呢?答案是什么都没有干就在等func执行结束,只有func执行结束main函数才能接着执行,所以这就是一个强耦合关系,这就好比去小孩放学回家在路上一定会买些吃边吃边回家,有些小孩会买辣条有些小孩会买炸火腿肠,买辣条的小孩买完就走了,而买炸火腿肠的小孩还得等,等火腿肠由生的变成熟的才能走,所以吃辣条的小孩会早些到家而买火腿肠的则会晚点到家,那么计算机中也是这样的,解耦一定会效率会高点而耦合则会效率低点所以这就是耦合的例子。

三者之间的关系

因为生产者和消费者都不止是一个线程,那么接下来我们就要讨论消费者和消费者之间的关系,生产者和生产者之间的关系,以及生产者和消费者之间的关系。生产者要将生产的东西放到超市里面,消费者要从超市里面拿东西,所以超市就是一个公共资源要被生产者和消费者访问,那么这里就存在一个问题,当超市的货架是空的生产者正要往货架上放东西而消费者这个时候正想拿东西的时候消费者能获取物品成功吗?答案是不确定的,理想状态下工作人员放东西要么就是放要么就是不放没有中间状态的,如果有中间状态他正在放东西的时候,他有没有放就决定着我能不能获取东西成功,而他有没有放我们是不能确定的,所以这里可能就会出现同时访问的问题,然后就照成了数据不一致的问题,这就好比供应商往超市的货架上放物品时可能存在很多步,比如说当前放了多少物品,这些物品编码是多少,生产日期是多少等等要记录很多步骤,而这个时候消费者拿取物品就可能会导致信息的错乱出现问题,因为超市里的某些资源是共享的所以就会出现资源的并发访问的问题,所以超市就得首先被保护起来,生产者在往超市里面存放东西的时候消费者不能从超市里面拿东西,而消费者在从超市里面拿东西的时候生产者就不能往里面放东西,那么这是生活中的例子我们还可以通过计算机中的例子再进行了解,比如说消费者线程想要从缓冲区里面读取数据hello world但是消费者刚读到hello的时候,生产者可能把之前的hello world改成hello history那这个时候就会消费者线程读取的数据就会出现问题,所以生产者和消费者之间的关系为互斥关系,那生产者和生产者之间是什么关系呢?答案是竞争关系比如说一个货架上面不能存放两个牌子的物品,在超市里面一个货架上要么是康师傅的方便要么是统一的防变量两家的绝对不能放到一起,所以在计算机当中生产者和生产者之间是互斥关系,消费者和消费者之间也是竞争关系比如说商品只剩下一份了但是两个消费者都想要这个商品那么这个时候消费者之间就是竞争关系也就是所谓的互斥。那么看到了这里我们就可以稍微的总结一下:

  1. 生产者和生产者之间的关系是互斥的
  2. 消费者和下消费者之间的关系是互斥的
  3. 消费者和生产者之间的关系是互斥的

消费者生产者模型改进

最近华为新出的mate60非常的火爆,那么我们这里就用遥遥领先来举一个例子,消费者小雷想去商店买一台遥遥领先用用,小雷的家离商店有点元素于是小雷花了好多时间来到商店,可是一问服务员发现遥遥领先卖完了没货了于是小雷只能失望的回家,第二天小雷又花了很多时间跑到商店询问瑶瑶领先有没有货,可是这个手机实在是太火爆了导致现在依然是缺货的状态,所以小王又只能这么无功而返,就这样第三天第四天第五天,小雷天天来找服务员购买mate60,但是服务员只能一次又一次的告诉小雷这个手机缺货了请明天再来看看,所以这样的行为
既浪费了消费者的时间也浪费了超市服务员的经历,同样的道理生产者生产商品的时候也会不停的询问超市是否需要该物品,如果一段时间不需要的话也势必会导致生产者不停的询问,这样既浪费了生产者的精力也浪费了商店服务员的精力,那么为了解决上述的问题商店的服务员可以和消费者生产者之间使用微信联系,当消费者想知道物品是否有货的时候就可以使用微信进行联系不需要亲自来到商店,生产者想知道是否需要补货的时候也可以使用微信进行联系而不需要亲自来到商店,所以这样就保证了生产者生产往商店里放了一部分,消费者就从商店里面拿走一部分,这样就让生产者和消费者之间协同了起来不仅提供了生产者的效率还提供了消费者的效率,生产者生产数据的时候会进行枷锁和解锁缓冲区,当缓冲区满了之后生产者又会枷锁和解锁的访问缓冲区能否装的下资源,如果生产者不停的循环枷锁解锁这就又会导致消费者的饥饿问题,反过来也是同样的道理,所以生产者和消费者之间的关系不仅存在着互斥的关系还存在着同步的关系,所以这里总结一下就是321原则:

3表示:
3种关系(消费者和消费者的关系(互斥), 生产者和生产者的关系(互斥),消费者和生产者的关系(互斥(保证共享资源的安全性),同步(保证生产者和消费的工作效率不然其中的其中一方处于饥饿状态不让其中一方干太多的无用功:询问是否有资源或者询问是否还能装的下资源))。

2表示
2种角色也就是生产者线程和消费者线程。

1表示
1个交易场所也就是一段特定结构的缓冲区。

那么只要我们想实现生产消费模型,本质上就是要维护好上面的321原则。

生产者消费者模型特点

  1. 生产线程和消费线程进行解耦。
  2. 解决生产和消费一段时间的忙闲不均的问题,比如说过年的时候工厂的工人都回家所以工厂的生产效率就非常的低,但是人们又要买很多的东西回家拜年,那么这个时候工厂的生产效率和消费者的消费效率就是不对等的,如果没有商店就会导致很多的人想买却买不着商品的问题,但是因为超市的存在他可以预先的存储一部分的商品,在工厂的生产效率特别低的情况的下也能满足消费者特别高的需求,所以这就解决了生产和消费的忙线不均的问题。
  3. 提高效率,比如说超市的存在提供了消费者和生产者的效率,如果没有超市消费者就只能自己前往遥远的工厂进行购买,而生产者也不敢大量的生产的物品怕消费者嫌弃太远不敢来了,那么在程序里面也是同样的道理:我们知道main函数在调用其他函数的时候是无法继续执行的,而函数可能会因为用户输入数据较慢的原因导致main线程一直处于等待状态,但是有了生产者和消费者模型就可以把main线程中用户输入数据的过程看成生产者,调用func函数执行func函数打印数据当成消费者,这样用户就可以往缓冲区中输入大量的数据即使某一刻不输入数据也不会导致func执行的情况,因为缓冲区中还存在数据可以继续供func函数执行这样就提高了程序的运行效率。那么这就是生产者消费者模型的特点。但是这个高效并不是绝对的比如说消费想要获取数据但是超市里面什么数据都没有这时就没有办法获取,同理生产者在生产数据的时候发现消费者正在拿数据,这时即使将数据生产出来了也没有办法将数据放进去,所以在一些场景下该模型就会演化成生产者生产一个消费者消费一个,那这个高效究竟高效在哪里呢?

条件变量的作用

在上面的学习中我们知道当超市里面的资源满了或者空了的时候,消费者和生产者可能就会不停的枷锁访问是否需要资源或者是否有资源然后再解锁,虽然这样保证了超市这个公共数据的安全,但是这样的现象不仅浪费了消费者的资源还浪费了生产者的资源,所以我们就让消费者和生产者之间为同步关系,那么为了实现这个同步的关系我们就得使用条件变量,当一个线程互斥地访问某个变量时,它可能发现在其它线程改变状态之前,它什么也做不了。例如一个线程访问队列时,发现队列为空,它只能等待,直到其它线程将一个节点添加到队列中时他才能获取到该节点,那么这种情况就需要用到条件变量。条件变量的作用就是当发现公共数据中没有数据时就让该线程挂起等待,等公共资源中有数据的时候再将等待的线程唤醒即可。

条件变量有关的函数

条件变量也是一个数据类型,使用条件变量之前得定义一个pthread_cond_t类型的对象,定义了该变量之后就可以使用pthread_cond_init来进行初始化,与锁相似如果条件变量是全局的话也可以使用PTHREAD_COND_INITIALIZER来初始化,该函数的声明如下:
linux入门---线程的同步_第4张图片
第一个参数就要传递一个条件变量的指针,第二个参数表示条件变量的属性这里不管直接传递null就可以,既然有条件变量的初始化那么就有条件变量的销毁,销毁就要用pthread_cond_destroy函数,想要销毁哪个变量就只需要传递对应变量的地址即可。生产者在访问资源的时候都是先枷锁然后判断生产的条件是否满足最后解锁,这就会导致其他线程的饥饿问题,但是有了条件变量之后就可以做到条件不满足的时候就不再申请锁资源了而是将自己挂起,那么这里的挂起用到的就是pthread_cond_wait函数该函数的声明如下,该函数的参数我们后面再聊:
在这里插入图片描述

既然有函数能在资源条件不满足的时候将线程挂起,那么当资源条件满足的时候一定存在一个函数可以将之前等待的线程唤醒那么这个函数就是pthread_cond_signal。
在这里插入图片描述

条件变量的理解

1.一个例子
公司招人的时候是需要面试的,并且每个岗位都会有大量的人来进行应聘,所以面试官所在的面试间外面就围着大量的人,每当面试完一个人的时候就让离房间门最近的一个人进来面试,但是这样的做法导致了面试的环境非常的乱,很多求职人员会因为谁靠的最近而吵来吵去争来争去,这既影响了面试时的环境也影响了面试的效率
linux入门---线程的同步_第5张图片

所以该公司就在离面试房间较远处插上了一个牌子,然后规定所有前来面试的人不要在面试房间周围等待,而是在这个牌子的左边按照先来后到的顺序排成一排进行等待:
linux入门---线程的同步_第6张图片
这样每面试完一个人就可以直接让牌左边的第一个人前往面试房间即可不需要进行挑选了,如果这个时候有人前来准备面试只用排到队伍的最左边即可,那么我们就把这里的牌子就称之为条件变量,所有应聘者等待面试的时候只能去条件牌子下面等待,而面试官叫人前来面试的时候也只会从牌子里面叫应聘者,那么这是现实生活在操作系统当中,如果共享资源的条件不满足的话,线程就只能去定义好的条件变量的上面进行等待,当共享资源的条件满足时便会去条件变量上面唤醒线程。

2.一张图
条件变量就是一个结构体变量,里面有个status表示当前条件变量的状态,还有一个task_struct指针用来维护的队列
linux入门---线程的同步_第7张图片
操作系统中存在多个线程这些线程在操作系统内部都是一个个的task_struct,所以当一个线程申请到了锁但是发现当前的资源不就绪的时候就可以将当前进程的task_struct链接到struct_cond中的task_struct队列里面
linux入门---线程的同步_第8张图片
等未来我们发现共享资源的条件满足的时候就可以调用pthread_cond_signal函数,该函数就是将线程的pcb从队列中取出来放到cpu中执行,那么这就是条件变量的基本结构。

条件变量的使用

那么这里我们就通过修改文章一开始的例子来带着大家了解一下条件变量有关函数的使用,首先原来代码的大致框架是这样:

#include
#include
#include
#include
using namespace std;
int ticket_num=1002;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
void * func(void* args)
{
}
int main()
{
    pthread_t tid1;
    pthread_t tid2;
    pthread_t tid3;
    pthread_create(&tid1,nullptr,func,(void*)"user1");
    pthread_create(&tid2,nullptr,func,(void*)"user2");
    pthread_create(&tid3,nullptr,func,(void*)"user3");
    pthread_join(tid1,nullptr);
    pthread_join(tid2,nullptr);
    pthread_join(tid3,nullptr);
    return 0;
}

我们就可以创建一个全局的条件变量

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond=PTHREAD_COND_INITIALIZER;

然后在func函数里面我们依然是先转换获取线程的名字,然后创建一个变量表示当前线程获得的票数总数,然后我们创建while循环表示不断的进行抢票,while循环的开始我们就先申请锁,一旦锁申请成功了我们就直接将该线程放到条件变量里面挂起,pthread_wait函数的声明如下:
在这里插入图片描述
第一个参数表示在哪个条件变量下面等待,第二个参数表示当前线程所持有的锁所以第二个参数我们传递锁的地址(至于为什么要传递锁的地址我们后面再聊),那么这里的代码如下:

void * func(void* args)
{
    string name=static_cast<const char*>(args);
    int sum=0;
    while(true)
    {
        pthread_mutex_lock(&mutex);
        pthread_cond_wait(&cond,&mutex);
    }
}

然后我们就判断一下如果当前的票数大于0就对ticket减减对sum的值++并打印一句话,否则就直接使用break跳出循环,不管是哪种情况都得将锁的资源释放,循环结束后就打印一下当前线程抢到票数的总和:

void * func(void* args)
{
    string name=static_cast<const char*>(args);
    int sum=0;
    while(true)
    {
        pthread_mutex_lock(&mutex);
        pthread_cond_wait(&cond,&mutex);
        if(ticket_num>0)
        {
            cout<<name<<" 正在进行抢票 "<< ticket_num<<endl;
            --ticket_num;
            sum++;
            pthread_mutex_unlock(&mutex);
        }
        else
        {
            pthread_mutex_unlock(&mutex);
            break;
        }
    }
    cout<<name<<"抢到票的个数:"<<sum<<endl;
    return nullptr;
}

因为我们在线程获取锁的下一步就将其添加到了条件变量里面他们没有办法自己将自己唤醒,所以就得在主线程里面调用pthread_cond_signal函数将条件变量中的线程一个一个的唤醒,该函数的声明如下:
在这里插入图片描述
参数表示你要唤醒哪个条件变量的线程,那么这里的代码就如下:

int main()
{
    pthread_t tid1;
    pthread_t tid2;
    pthread_t tid3;
    pthread_create(&tid1,nullptr,func,(void*)"user1");
    pthread_create(&tid2,nullptr,func,(void*)"user2");
    pthread_create(&tid3,nullptr,func,(void*)"user3");
    for(int i=0;i<1002+3;i++)
    {
        usleep(1234);
        pthread_cond_signal(&cond);

    }
    pthread_join(tid1,nullptr);
    pthread_join(tid2,nullptr);
    pthread_join(tid3,nullptr);
    return 0;
}

因为这里的3个线程在ticket为0的时候也会申请锁然后去条件变量下等待,所以这里得多加三次那么这里我们的代码就修改完了,程序运行的结果如下:
linux入门---线程的同步_第9张图片
可以看到这里的运行结果符合我们的预期,那么这就是信号量的作用和用法,在上面的图片中我们还遇到一个叫
pthread_cond_broadcast函数这函数的作用也是唤醒线程,pthread_cond_signal函数是一次唤醒一个线程,那么这个函数的作用就是一次唤醒所有等待的线程,比如说将代码修改成下面这样:

for(int i=0;i<1002+3;i++)
{
    sleep(1);
    pthread_cond_broadcast(&cond);
    cout<<"----------------------"<<endl;

}

运行的结果如下:
linux入门---线程的同步_第10张图片
可以看到一次性唤醒了三个线程那么这就是信号量的使用,最后我们来谈谈为什么信号量挂起函数得传递一个锁指针,原因很简单你被挂起了其他线程还得申请这个锁对公共资源进行访问,所以得传递这个锁指针来释放你之前申请的锁资源供其他线程使用,等你被唤醒的时候又会将你之前申请的锁资源还给你,那么这就是本篇文章的全部内容。

你可能感兴趣的:(linux入门,linux,算法,运维)