初识linux之线程同步与生产者消费者模型

news2025/3/10 22:15:34

目录

一、线程同步的概念

1. 饥饿状态

2. 同步的概念

二、生产者消费者模型

1. 生产者消费者模型基本概念

2. 生产者、消费者之间的关系

2.1 消费者与消费者的关系

2.2 生产者和生产者的关系

2.3 生产者和消费者的关系

3. “321”原则

4. 消费者与生产者模型的特点

4.1 生产线程和消费线程解耦

4.2 支持生产和消费一段时间的忙闲不均问题

4.3 提高效率

三、条件变量

1. 条件变量的使用

1.1 条件变量的初始化和销毁

1.2 条件变量的等待

1.3 条件变量的唤醒

2.理解条件变量

3. 测试使用条件变量

4.用阻塞队列模拟实现一个简单的生成消费模型

4.1 阻塞队列文件

4.2 任务文件

4.3 测试文件

5. 消费者生产者模型的高效问题


一、线程同步的概念

1. 饥饿状态

多线程并发进入一个不可重入的临界资源时,可以使用互斥让线程串行访问该区域,但是互斥并不是每次在任何类似场景都可以使用的,因为互斥是让线程竞争锁,这也就意味着,互斥的线程是谁先拿到票就谁先访问。这就可能导致某个进程或某些进程的竞争力比较高频繁访问资源,使得其他线程抢不到锁而无法访问资源。此时这些线程就处于饥饿状态

举个例子,假设你的学校里面有一个豪华自习室,里面的设备和环境非常好,并且这个自习室的钥匙就放在门口的一张桌子上,任何人都可以拿着这把钥匙打开自习室进去自习。但是它每次只允许一个人进去自习

而你是个卷王,每天凌晨4点就起床去这个自习室。当你从宿舍走到自习室时,发现自习室没人,于是你拿着钥匙去打开自习室的门,进去把门反锁然后在里面自习。在你自习的这段时间里,不断有人从宿舍赶到自习室门口,发现里面有人后,就在门口等着,等你离开自习室他好去拿到钥匙进去自习。

当你自习了一个小时后,你想出门上个厕所,你打开门发现外面有很多人都等着,但是没有关系,现在只有你有自习室的钥匙,于是你带着钥匙大摇大摆的去上了个厕所再回自习室。在你上厕所的这段时间里,其他人因为没有钥匙而无法进入,只能看着你上完厕所重新返回自习室。

当你又自习了1个小时后,你想出门去吃个早餐。你打开自习室的门一开,发现外面站着许多人,每个人都看着你,等你放下钥匙。于是你走到桌子面前,刚把钥匙放到桌子上,你转念一想,你4点钟就起床过来占这个位置,才自习了两个小时就走,是不是太亏了。此时你刚放下钥匙,钥匙离你最近,于是你又拿起钥匙起身走回自习室继续自习了,而其他人因为竞争不过你,只能看着你进去。

当你又自习一段时间后,你又从自习室出来,刚把钥匙放下后,又拿起钥匙起身走回了自习室。在接下来的1个多小时里,你都在重复这个行为。于是,在这一个多小时里,其他人就只能看着你时不时的拿着钥匙出来,放下钥匙后又拿起钥匙回去。他们虽然心生不满,但因为竞争不过你而什么都做不了。此时这种其他人看着你反复进入自习室而无法使用自习室的状态,就叫做“饥饿状态”。

2. 同步的概念

以上面的那个例子为例,同步就是学校的自习室管理员发现因为某个同学频繁拿着钥匙出入自习室导致其他人都无法使用自习室的问题,于是出台了一个规定,即当自习室有人使用时,后面来的人如果想使用自习室,都必须排队进入。而进了自习室的同学,只要出了这个自习室,就必须归还钥匙让下一个人进去。如果想再次进去,就需要到后面重新排队。

因此,同步,其实就是在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问题。

二、生产者消费者模型

1. 生产者消费者模型基本概念

生产者消费者模型,其实就是使传输数据与使用数据的双方解耦。为什么要解耦呢?举个例子,在我们代码时,会先写一个主函数main,假设在这个主函数中有一个函数func。当main运行到func的时候,main就会将func需要的数据传输给它,func就拿着这些数据去形成变量,进行各种操作。当func在运行的时候,main什么都干不了,只能等待func的结束。这种一方的运行很容易受到另一方运行的影响的关系,就叫做“强耦合”关系

而生产者消费者模型,就是会提供一个临时保存数据的缓冲区,生产数据的一方将数据填入缓冲区,消费数据的一方从缓冲区拿数据。通过这种方式,让双方不再强依赖对方的生产运行速度,实现双方的解耦。

这样来讲大家可能不太好理解。举个例子。在学校里面,都会有一个超市,假设在这个超市里面有很多火腿肠,而你和你的同学非常喜欢吃火腿肠,于是经常都会去买。那这些火腿肠是由超市生成的吗?并不是,而是由工厂生产的,工厂将生产出来的火腿肠大批量的放入到超市中,以提供需求。

在这个例子里面,学生就是要拿取数据的多个线程,工厂就是要生产数据的多个线程;而超市,则是一个缓冲区,用于为双方提供一个存放和拿取数据的区域。

2. 生产者、消费者之间的关系

2.1 消费者与消费者的关系

还是上面的例子,假设有一天,超市里面的火腿肠卖完了,但是你不知道,于是你走到超市里面,发现货架上已经没有火腿肠了,但是刚好有一个工作人员在向上面放火腿肠。此时又来了一个同学,也发现货架上没有火腿肠,于是他也走过来。此时你和他就同时看到了一份火腿肠,都想将它拿走,你和他之间就处于竞争关系。要避免这种情况,就不能让多个消费者同时进到超市的火腿肠货架上看到同一个火腿肠。

因此,消费者和消费者之间的关系,就是“互斥关系”

2.2 生产者和生产者的关系

在为超市提供货源的工厂中,每个工厂都生成不同品牌的火腿,一个生成金华火腿,一个生产双汇火腿。虽然它们都生成火腿,这两个工厂都想将自己的火腿摆进去。但超市的货架并不大,只能摆下一种火腿。此时这两个工厂直接就要竞争这个货架,决定放谁的火腿肠。此时,它们之间就处于竞争关系。要避免这个问题,就不能让几个工厂同时向超市放火腿肠。

因此,生产者和生产者之间的关系,也是“互斥关系”

2.3 生产者和消费者的关系

依然是上面的例子。假设有一天,你在去超市买火腿肠的时候,正好工厂的工作人员正在往货架上摆火腿肠。当你走到货架上刚准备拿一个火腿肠的时候,工作人员走过来把你要拿的金华火腿肠换成了双汇火腿肠。但你因为正在玩手机,没看到这一行为,于是你拿起火腿肠走过去结账,等到了宿舍要吃的时候才发现你买的火腿肠的牌子不对。但你此时已经拆开了火腿肠,没办法退货,只能自认倒霉。这就是因为消费者和生产者同时访问超市导致的。

这就好像消费者线程在去缓冲区拿数据时,生产者线程也在传输数据,此时消费者线程就可能拿到错误的数据并带回去使用。因此,生成者和消费者的关系首先是“互斥关系”

假设某一天,超市里面的火腿肠卖完了,但是你并不知道,于是你走到超市里面,看到货架上没有火腿肠后,就跑去询问超市的工作人员火腿肠什么时候才到。超市人员说工厂放假了,他也不知道,于是你就回去了。等到了第二天,你又跑去问,超市的人还是回答不知道。就这样过了一个月,每天你都会跑去询问工作人员火腿肠到没到。

在这个过程中,即浪费 了你的时间,也浪费了超市的时间。因为你每天去问一趟,路上要花费时间,而超市的工作人员每次回到这个问题,也要花费时间,假设一天里有1000个人在问这个问题,超市的工作人员就要回答1000次,浪费了大量时间。于是超市的工作人员就想了个办法,他加了来询问火腿肠的人的微信,然后告诉你们,当火腿肠到货时,他就通过微信告诉你们,你们就不用每天都过来问了。

而工厂那边在结束放假后,就开始马不停蹄的进行生产。每生产出一批火腿肠后,工厂的人就去问超市的人超市的货架还放不放得下,几乎每天如此。超市的工作人员就感到很烦躁,于是就加了工厂的人的微信,告诉他们,当超市需要火腿肠时,就在微信上告诉你们,你们把货运过来就行,别时不时的过来问了。

此时,超市的工作人员就同时拥有了消费者和生产者的联系方式,每当有货或缺货的时候就微信上告诉对方,无需对方时不时的过来询问。在这个工程中,就实现了消费者和生产者的“同步”

因此,消费者和生产者之间的关系就是既要互斥,也要同步,即“互斥与同步关系”

3. “321”原则

生产者和消费者模型,总结起来就是要遵循“321”原则

3,指的是三种关系——消费者与消费者(互斥)、生产者与生产者(互斥)、消费者与生产者(互斥(保证共享资源安全)与同步)

2,指的是两个角色——消费者线程与生产者线程

1,指的是一个交易场所——缓冲区

因此,当要实现一个消费者生产者模型,就是要维护321原则。

4. 消费者与生产者模型的特点

4.1 生产线程和消费线程解耦

通过提供一段缓冲区的方式,使得消费线程无需直接向生产线程要数据,生产线程也无需直接向消费线程传输数据。

4.2 支持生产和消费一段时间的忙闲不均问题

生产线程和消费线程的生产数据和消费数据也是有快慢之分的。

就好比消费者到超市买东西,在节假日时大家都放假了,于是超市的消费者就比较多,商品消费速度快。但到了工作日,大家都要上学和工作,消费速度就会降低。

工厂也是如此。工厂在日常中时,大家都在进行生产,生产速度比较快。但是到了节假日的时候,很多人都要放假回家了,此时工厂内工人不足,生产速度就会降低。

因此,超市就可以通过调节这一状况,让工厂在工作日时多生产点商品,它好在工作日人少的时候将商品摆上去。等到了周末的时候,就让工厂不要再送商品过来了,而是集中精力为消费者提供商品。

因此,在消费者生产者模型中,消费者线程和生产者线程在消费数据和生产数据时也会有快慢之分。此时就可以通过让缓冲区判断其内部数据的多少,在某个时间段集中提供数据或获取数据,在一定程度上解决消费者线程和生产者线程的忙闲不均问题

4.3 提高效率

通过消费者和生产者模型,让消费线程和生产线程可以通过缓冲区集中拿取和存放数据。这一方式就比让生产线程在生产出数据后就立刻传输给消费线程效率更高。

三、条件变量

在消费者和生产者模型中,消费者和生产者之间是要保持“互斥与同步关系”的。这就表示在生产线程进入缓冲区之前,需要进行加锁,在离开缓冲区时再进行解锁。但这就会导致一个问题,生产者在里面生产数据后,发现缓冲区满了,于是它上面都不做,就离开了。但是此时生产线程离锁比较近,竞争力较高,所以在下一次又是生产者线程进去。但缓冲区内的数据并没有被消费者线程拿走,还是满的,于是生产线程就又离开了,继续重复上述过程。这就会导致消费者线程一直无法消费数据,生成线程也一直无法写入数据。

由此,就需要使用条件变量来避免。

条件变量的逻辑就是,在一个加锁的区域内,让线程通过这个条件变量来判断,如果不满足条件,就等待,满足条件,就继续或唤醒

1. 条件变量的使用

条件变量的本质,其实就是一个数据类型。总的来讲,条件变量的使用和锁的使用是很相似的。

1.1 条件变量的初始化和销毁

要使用条件变量,首先需要定义个pthread_cond_t的变量,对这个变量进行初始化后,就可以使用了。

要初始化条件变量,就需要使用“pthread_cond_init()”函数:

第一个参数是输出型参数,要传入一个参数进行接收。第二个参数是条件变量的属性,一般填为nullptr即可。

注意,和锁一样,条件变量在函数内使用时,需要用pthread_cond_init()进行初始化。但是如果是在全局域定义一个条件变量,直接使用“PTHREAD_COND_INITIALIZER”宏初始化即可。

 如果要销毁一个条件变量,使用pthread_cond_destory()接口即可。

1.2 条件变量的等待

当一个线程进入后,如果不满足条件变量,就需要将自己挂起,进入阻塞等待。而要让某个线程进入阻塞等待,使用“pthread_cond_wait()”函数即可:

第一个参数是要判断的条件变量,第二个参数是线程此时持有的锁

上面的函数是让一个线程一直等待直到被唤醒,如果想让函数在某个时间段内等待,时间段结束就返回,可以使用“pthread_cond_timedwait()”函数:

该函数的第一个参数是条件变量,第二个参数是,第三个参数就是要等待的时间段

1.3 条件变量的唤醒

既然一个线程可以被等待,那么在满足条件变量时,就需要将其唤醒。要唤醒一个线程,就需要使用“pthread_cond_signal()”函数:

 该函数只能唤醒一个线程。如果想唤醒持有该条件变量的所有线程,就可以使用“pthread_cond_broadcast()”函数:

2.理解条件变量

在这里先举一个例子。假设未来大家找工作时,你通过了笔试要去参加面试。而要面试的公司将面试地点设置在一家酒店里面。当你去面试的时候,发现面试的地方已经有很多人了,而面试的房间里面一次只能面试一个人。于是大家都站在门口,等待里面面试的人出来。但是由于面试的地方没有人管理,所以当一个面试的人出来后,他可能觉得自己的面试不理想,于是又偷偷的折返回去重新面试。而面试官也没有注意到这个问题,就给他重新面试了。此时,就是就会导致一个人反复面试使得其他人无法进入面试。

看到这个情况,于是hr去找了个桌子,将这个桌子放在面试室外面,设置为等待区。然后站出来说,全部来面试的人都需要在他这里进行排队,不排队就不允许面试。面试完的人如果觉得面试结果不理想,出来后可以重新面试,但必须要到后面排队,不允许立刻重新进入面试室。每当面试室里面的面试官说下一个的时候,hr才会让排在第一位的人进去面试。

此时,这个hr就是条件变量,管理着来面试的人,让他们有序的排队等待进入。

 在这里,这一个个来面试的人就是线程的PCB,hr就是条件变量。所有调用了pthread_cond_wait()函数的线程,此时被判定为等待状态,依次进入等待区进行等待,可以看成被放在一个队列里面。

当满足条件后,就让对应的线程调用pthread_cond_signal()函数,将该线程唤醒,让它去执行代码:

3. 测试使用条件变量

写入如下测试程序:

在这个测试程序里面,让4个线程轮番进入getTicket()函数中执行代码。在执行代码时,该程序会对进入该函数的线程上锁并用条件变量使其进入等待队列。当唤醒一个线程后,这个线程才能向下运行。运行该测试程序:

可以发现,这个程序的所有线程都会按照一定次序访问临界区。这个次序,就是这些线程进入临界区被上锁和进入等待队列中的次序。

注意,上面的条件变量在使用时是没有进行判断的,在实际使用中,可以加上判断,让不满足某些条件的线程进入等待。

4.用阻塞队列模拟实现一个简单的生成消费模型

阻塞队列,是多线程编程中一种常用语实现生产者和消费者模型的数据结构,其与普通的队列区别在于,当队列为空时,从队列获取元素的操作会被阻塞,直到队列中别放入了元素当队列满时,往队列里存放元素的操作也会被阻塞,直到有元素别从队列中取出。 

在生产消费模型中,我们需要维护三种关系,在这里我们先维护一种关系——消费者与生产者的互斥与同步关系。

4.1 阻塞队列文件

#pragma once
#include<iostream>
#include<string>
#include<queue>
#include<pthread.h>
#include<unistd.h>
#include<time.h>
#include<stdlib.h>

using namespace std;

const int gmaxcap = 5;

template<class T>
class blockqueue
{
public:
    blockqueue(const int&  maxcap = gmaxcap)//构造函数
    :_maxcap(maxcap)
    {
        pthread_mutex_init(&_mutex, nullptr);//初始化锁

        pthread_cond_init(&_pcond, nullptr);//初始化生产线程的条件变量
        pthread_cond_init(&_ccond, nullptr);//初始化消费线程的条件变量
    }

    void push(const T& in)//传入数据,一般来讲,使用输入性参数,一般用const &
    {
        pthread_mutex_lock(&_mutex);//上锁
        //注意,充当条件判断的语句必须使用while,不能使用if。因为在未来可能会有多个线程写入数据,如果用
        //if,那么这些线程就是在if里面被挂起,当被全部唤醒时,所以的线程都会向下执行,此时就可能
        //因为调度的问题写入错误的数据。使用while保证一次只有一个线程离开循环
        while(full())//为满
        {
            //pthread_cond_wait()在挂起一个线程时,会以原子性的方式释放锁,然后将该线程挂起
            //当一个线程被唤醒时,它会继续执行pthread_cond_wait()中的代码,让该线程重新获取锁
            pthread_cond_wait(&_pcond, &_mutex);//生产进程进入等待
        }
        _q.push(in);//传入数据

        pthread_cond_signal(&_ccond);//有数据可拿,唤醒消费进程

        pthread_mutex_unlock(&_mutex);//解锁
    }

    void pop(T* out)//拿走数据.一般来讲,使用输出型参数,一般使用*  //如果是输入输出型,则使用&
    {
        pthread_mutex_lock(&_mutex);//上锁
        //注意,充当条件判断的语句必须使用while,不能使用if。因为在未来可能有多个消费线程消费数据。如果使用if,这些消费线程
        //就会在if内被挂起。当全部线程被唤醒时,就会有多个线程向下拿走数据,此时就极可能拿到错误的数据。使用
        //while保证线程被唤醒时,只有一个线程离开循环拿取数据
        while(empty())//数据为空
        {
            pthread_cond_wait(&_ccond, &_mutex);//让自己进入等待
        }
        *out = _q.front();//拿走数据
        _q.pop();//删除等待队列中的对应数据

        pthread_cond_signal(&_pcond);//缓冲区有位置可写,唤醒消费线程
        pthread_mutex_unlock(&_mutex);//解锁    
    }

    bool empty()//判空
    {
        return _q.empty();
    }

    bool full()//判满
    {
        return _q.size() == _maxcap;
    }

    ~blockqueue()//析构函数
    {
        pthread_mutex_destroy(&_mutex);//销毁锁

        pthread_cond_destroy(&_pcond);//销毁生产线程的锁
        pthread_cond_destroy(&_ccond);//销毁消费线程的锁
    }

private:
    queue<T> _q;//队列
    int _maxcap;//队列的最大容量
    pthread_mutex_t _mutex;//锁,保护线程安全
    pthread_cond_t _pcond;//生产线程的条件变量
    pthread_cond_t _ccond;//消费线程的条件变量
};

4.2 任务文件

为了测试这个阻塞队列是否能够正常运行,就让阻塞队列做两件事,一个是计算任务,计算传入的值;另一个是存储任务,将计算结果存储到一个文件中。

#include<iostream>
#include<functional>
#include<string>
#include<stdio.h>

class CalTask//计算任务
{
    typedef std::function<int(int, int, char)> func_t;//函数指针
public:
    CalTask()
    {}

    CalTask(const int& x, const int& y, const char& op, func_t func)
    :_x(x), _y(y), _op(op), _func(func)
    {}

    std::string operator()()
    {
        int result = _func(_x, _y, _op);//外部传入的计算函数

        char buffer[64];
        snprintf(buffer, sizeof(buffer), "%d %c %d = %d", _x, _op, _y, result);

        return buffer;
    }

    std::string tostring()
    {
        char buffer[64];
        snprintf(buffer, sizeof(buffer), "%d %c %d = ?", _x, _op, _y);

        return buffer;
    }

private:
    int _x;
    int _y;
    char _op;
    func_t _func;
};

std::string oper = "+-*/%";//提供五种计算方法

int mymath(int x, int y, char op)//计算方法
{
    int result = 0;
    switch(op)
    {
        case '+':
        {
            result = x + y;
            break;
        }
        case '-':
        {
            result = x - y;
            break;
        }
        case '*':
        {
            result = x * y;
            break;
        }
        case '/':
        {
            if(y == 0)
            {
                cout << "div zero error!" << endl;
                result = -1;
            }
            else
                result = x / y;    
            
            break;
        }
        case '%':
        {
            if(y == 0)
            {
                cout << "mod zero error" << endl;
                result = -1;
            }
            else
                result = x % y;

            break;           
        }
        default:
            break;
    }

    return result;
}

class SaveTask//存储任务
{
    typedef std::function<void(std::string&)> func_t;//执行将数据写到文件的函数指针
public:
    SaveTask()
    {}

    SaveTask(std::string message, func_t func)
    :_message(message), _func(func)
    {}

    void operator()()
    {
        _func(_message);
    }

private:
    std::string _message;//要写入文件的数据
    func_t _func;
};

void save(const std::string& message)//保存方法
{
    const std::string target =  "./log.txt";//文件名
    FILE* fp = fopen(target.c_str(), "a+");//以追加方式打开文件
    if(!fp)
    {
        std::cout << "fopen error!" << endl;
        return;
    }

    fputs(message.c_str(), fp);
    fputs("\n", fp);
    fclose(fp);
}

4.3 测试文件

在这个测试文件里面,主要做两件事。首先是要创建三个线程,让这三个线程分别执行生成任务,获取任务和存储任务三个工作。

执行生成任务的线程要需要的数据和计算方法传输到计算队列中;执行获取任务的线程要从计算队列中获取任务并完成计算。在完成计算后要再将得到的数据推送到存储队列中;执行存储任务的线程要从存储队列中获取存储任务并执行。

#include"BlockQueue.hpp"
#include"Task.hpp"

template<class C, class S>
struct BlcokQueues
{
    blockqueue<C>* c_bq;//计算队列
    blockqueue<S>* s_bq;//存储队列
};

void* producer(void* args)//生成
{
    blockqueue<CalTask>* bq = (static_cast<BlcokQueues<CalTask, SaveTask>*>(args))->c_bq;//将计算队列传过来

    while(1)
    {
        //生产活动
        int x = rand() % 10 + 1;
        int y = rand() % 5 + 1;
        int operCode = rand() % oper.size();//随机选择一种计算方法

        CalTask t(x, y, oper[operCode], mymath);

        bq->push(t);
        cout << "producer: 生产任务: " << t.tostring() << endl;
        sleep(1);
    }

    return nullptr;
}

void* consumer(void* args)//消费
{
    blockqueue<CalTask>* cal_bq = (static_cast<BlcokQueues<CalTask, SaveTask>*>(args))->c_bq;//将计算队列传过来
    blockqueue<SaveTask>* save_bq = (static_cast<BlcokQueues<CalTask, SaveTask>*>(args))->s_bq;//将计算队列传过来

    while(1)
    {
        //消费活动,获取任务
        CalTask ct;
        cal_bq->pop(&ct);

        std::string result = ct();

        cout << "consumer: 消费任务: " << result << "...done" << endl;

        SaveTask st(result, save);//构建存储任务
        save_bq->push(st);//将存储任务推送到存储队列

        cout << "consumer: 推送任务完成..." << endl;
    }

    return nullptr;
}

void* saver(void* args)//保存
{
    blockqueue<SaveTask>* save_bq = (static_cast<BlcokQueues<CalTask, SaveTask>*>(args))->s_bq;//拿到存储队列

    while(1)
    {
        SaveTask st;
        save_bq->pop(&st);//拿到存储任务
        st();//执行存储任务

        cout << "saver: 存储任务完成..." << endl;
    }

    return nullptr;
}

int main()
{
    srand(time(0));
    BlcokQueues<CalTask, SaveTask> bqs;
    bqs.c_bq = new blockqueue<CalTask>();//创建计算队列
    bqs.s_bq = new blockqueue<SaveTask>();//创建存储队列

    pthread_t p, c, s;//接收线程id
    pthread_create(&p, nullptr, producer, (void*)&bqs);//生产线程
    pthread_create(&c, nullptr, consumer, (void*)&bqs);//消费线程
    pthread_create(&s, nullptr, saver, (void*)&bqs);//存储线程

    pthread_join(p, nullptr);
    pthread_join(c, nullptr);
    pthread_join(s, nullptr);//线程等待

    delete bqs.c_bq;
    delete bqs.s_bq;
    return 0;
}

有了以上三个文件后,就可以运行程序,测试阻塞队列是否能正常运行:

运行后可以发现,这些线程确实可以实现有序的完成任务。符合预期。再打开“log.txt”文件,看看数据是否被正确写入文件中:

数据也没有问题。这也就说明上面的这个消费者生产者模型的代码实现并没有什么问题。它的实现中所需要注意的一些问题在代码中也有注释,这里就不再赘述了。

当然,这个模拟实现的消费者生产者模型也是可以支持多消费者多生产者同时访问的。因为在该模型的push()和pop()函数中已经用锁和条件变量进行了保护,可以适应多线程的情况。

5. 消费者生产者模型的高效问题

在消费者生产者模型中,消费线程和生产线程都是必须互斥式的拿数据和传输数据的,这就意味着,其实消费线程和生产线程在使用缓冲区时,都是串行访问的。既然如此,消费者生产者模型的高效,到底高效在哪里呢?

其实消费者生产者模型的高效并不是高效在消费线程和生产线程从缓冲区拿数据。在生成线程中,上面的代码走构建传给任务的数据和形成任务时,只是调用了一个rand()函数生成随机值。但是在未来,生成线程的数据可能会从网络、数据库等等地方拿取数据,拿取数据后再构建任务并将任务传入。而拿取数据的过程可能就是比较耗时的。该模型就可以支持就算缓冲区内的数据满了,其他生产线程虽然无法继续传入数据,但依然可以继续获取数据构建任务;消费线程同理,消费线程在执行任务的时候可能非常耗时,该模型就可以支持消费线程串行拿取任务后,如果某些线程还在执行任务,也不影响其他消费线程拿取任务和执行任务,使得多个消费线程并发进行不同的运算

因此,消费者生产者模型的高效就是高效在支持多个生产线程并发拿取数据、构建任务和消费线程并发执行不同的运算

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/433452.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

C++数据结构:STL

数据结构和算法简介 数据结构 数据结构是相互间存在特定关系的数据的集合&#xff0c;分为逻辑结构和物理结构。 逻辑结构 反映数据元素之间的逻辑关系的数据结构&#xff0c;其中的逻辑关系是指数据元素之间的前后件关系&#xff0c;而与他们在计算机中的存储位置无关 集…

类加载器详解(重点)之双亲委派

回顾一下类加载过程 开始介绍类加载器和双亲委派模型之前&#xff0c;简单回顾一下类加载过程。 类加载过程&#xff1a;加载->连接->初始化。连接过程又可分为三步&#xff1a;验证->准备->解析。 加载是类加载过程的第一步&#xff0c;主要完成下面 3 件事情…

多线程基础

1.多线程基础概念 多线程&#xff1a;让程序同时做多件事情 多线程作用&#xff1a;提高效率 并发&#xff1a;在同一时间&#xff0c;有多个指令在单个cpu上交替执行 并行&#xff1a;在同一时刻&#xff0c;有多个指令在多个cpu上同时执行 2.多线程的实现 (1)继承Thread类…

计算机网络问题

1.网络分层结构及其必要性 五层体系结构&#xff1a; 七层结构&#xff1a; 应用层&#xff1a;网络服务与最终用户的一个接口&#xff0c;常见的协议有&#xff1a;HTTP FTP SMTP SNMP DNS.表示层&#xff1a;数据的表示、安全、压缩。&#xff0c;确保一个系统的应用层所发…

『Linux笔记』Linux设置SSH远程连接Docker容器

Linux设置SSH远程连接Docker容器 文章目录 一. 创建容器二. 进入容器/设置密码三. 安装ssh及修改配置四. 重启ssh服务五. 远程连接六. 提交运行中的容器docker commit参考文章 一. 创建容器 在ubuntu镜像中创建容器&#xff0c;并将docker服务器的60222端口映射到容器的22端口…

如何将Tomcat集成到IDEA中并启动项目?

编译软件&#xff1a;IntelliJ IDEA 2019.2.4 x64 操作系统&#xff1a;win10 x64 位 家庭版 服务器软件&#xff1a;apache-tomcat-8.5.27 目录一、为什么要将Tomcat集成到IDEA里&#xff1f;二、集成步骤2.1 在IDEA中创建Tomcat2.2 创建java的企业级模块&#xff08;动态的we…

同为科技(TOWE)8路RS485通讯智能机柜PDU时序电源管理器

智能PDU电源时序管理器的出现是为了更好地管理数据中心和服务器机房的电源&#xff0c;以提高其可靠性和效率。在传统的机房电力管理中&#xff0c;运维人员需要手动控制每个设备的电源开关和电力分配&#xff0c;这种方式不仅效率低下&#xff0c;而且容易出现操作失误和电源供…

记一次mysql cpu 异常升高100%问题排查

此服务器为一个从库&#xff0c;用于数据的导出业务&#xff0c;服务器配置较低&#xff0c;日常的慢sql也比较多。 上午11点左右cpu异常告警&#xff0c;如下图所示&#xff0c; cpu使用率突增到50%&#xff0c;下午2点左右突增到100% &#xff0c;登录服务器top命令查看cpu升…

基于脚手架@vue/cli 5.0.8搭建vue3项目教程

基于脚手架vue/cli 5.0.8搭建vue3项目教程 前言 前言 脚手架可以快速的帮我们搭建一个项目&#xff0c;而不需要我们从头开始去配置和引入插件&#xff0c;使用脚手架5.0.8版本创建的项目&#xff0c;局部webpack是5.x版本的&#xff0c;因此所有的配置均需要使用支持5.x版本的…

Shader Graph11-Detail Normal Map(法线贴图叠加)

本次我们希望通过叠加两个法线贴图来增加细节。 一、准备资源 我们需要一个模型&#xff0c;1张纹理贴图&#xff0c;2张法线贴图 我们可以在商店下载&#xff0c;这个mesh资源来做这个例子 打开OldWest->VOL3->Meshes&#xff0c;找到SM_Blankets_01c这个布的模型&am…

QoS技术原理

QoS技术的产生 随着网络的不断发展&#xff0c;网络规模及流量类型的不断增加&#xff0c;使得互联网流量激增&#xff0c;产生网络拥塞&#xff0c;增加转发时延&#xff0c;严重时还会产生丢包&#xff0c;导致业务质量下降甚至不可用。所以&#xff0c;要在IP网络上开展这些…

Abaqus 2022最新版下载软件安装包 永久安装包详细安装流程

Abaqus 2022是一套功能强大的工程模拟的有限元软件&#xff0c;其解决问题的范围从相对简单的线性分析到许多复杂的非线性问题。Abaqus包括一个丰富的、可模拟任意几何形状的单元库。并拥有各种类型的材料模型库&#xff0c;可以模拟典型工程材料的性能&#xff0c;其中包括金属…

浏览器网络之TCP与UDP

文章目录 网络模型TCP协议建立连接——三次握手断开连接——四次挥手为什么要三次握手与四次挥手 不常使用的协议——UDPTCP与UDP的区别UDP的使用场景 总结 网络模型 在理解TCP与UDP 之前&#xff0c;首先需要对网络结构有一些基本的认识&#xff0c;在互联网发展的初期&#…

【golang学习笔记】——(二)配置golang vscode开发环境

本次学习采用vscode进行开发&#xff0c;vscode有比较成熟的插件&#xff0c;使用起来比较方便。 一、插件下载 搜索“Go”&#xff0c;第一个插件即为go的开发插件&#xff0c;进行安装即可。 二、go辅助插件下载 使用快捷键&#xff1a;CtrlShiftP 输入Go:Install/Update T…

SpringCloud之组件Hystrix简介

服务雪崩介绍 服务提供者不可用导致服务调用者也跟着不可用&#xff0c;以此类推引起整个链路中的所有微服务都不可用&#xff0c; 服务提供者A因为某种原因出现故障&#xff0c;那么服务调用者服务B依赖于服务A的请求便无法成功调用其提供的接口&#xff0c;假以时日依赖于服务…

电子表格软件能解决什么问题?

在当下的时代&#xff0c;全球经济非常活跃&#xff0c;对于企业来说&#xff0c;经营的成果需要“用数字说话”——以数字来反映经营管理的的状况&#xff0c;这些都离不开报表&#xff0c;可以说报表的需求是无处不在的。 在企业所有的科技类项目中报表是最基本的功能。作为…

第二章:uniapp整合axios之真机测试两问题

第二章&#xff1a;uniapp整合axios之真机测试两问题 上一章节&#xff0c;笔者编写了uniapp整合axios并实现前后端跨域请求的方案&#xff0c;完成了这些基本配置后&#xff0c;在浏览器端的测试基本是可以完成了&#xff0c;但是当笔者将程序运行到手机时&#xff0c;却出现…

4个 Python 库来美化你的 Matplotlib 图表

Matplotlib是一个被广泛使用的Python数据可视化库&#xff0c;相信很多人都使用过。 但是有时候总会觉得&#xff0c;Matplotlib做出来的图表不是很好看、不美观。 今天我就给大家分享四个美化Matplotlib图表的Python库&#xff0c;它们可以轻松让你的Matplotlib图表变得好看…

fastjson 反序列化之mysql JDBC 利用

前言&#xff1a; 在打春秋云境Exchange 靶场时&#xff0c;入口点是华夏ERP 2.3版本系统&#xff0c;存在fastjson 反序列化漏洞&#xff0c;在尝试常见的fastjson利用链反弹shell都没有反应&#xff0c;最终使用mysql JDBC利用链反弹shell成功。在此记录一下。 复现本地靶场…

QT安装mysql驱动和使用ODBC连接mysql

文章目录 QT安装mysql驱动和使用ODBC连接mysql使用驱动连接mysql编译mysql驱动连接mysql 使用ODBC连接mysql QT安装mysql驱动和使用ODBC连接mysql 上一篇博文中提到了mysql的使用&#xff0c;但是很多人在使用新版Qt连接mysql的时候出现连接不上或者是没有mysql驱动的问题&…