基于阻塞队列的生产消费模型

news2024/11/27 20:21:31

目录

一、线程同步

1.生产消费模型(或生产者消费者模型)

2.认识同步

(1)生产消费模型中的同步

(2)生产者消费者模型的特点

二、条件变量

1.认识条件变量

2.条件变量的使用

3.代码改造

三、基于阻塞队列的生产消费模型

1.阻塞队列类

(1)阻塞队列

(2)实现生产者的生产函数

(3)实现消费者的消费函数

2.pthread_cond_wait为什么要传入锁

3.生产者和消费者线程的执行函数

(1)执行函数

(2)试运行

4.部分细节处理

(1)伪唤醒问题

(2)解锁与唤醒的顺序

5.处理任务的生产消费模型

(1)代码改造

(2)运行

6.生产消费模型为何高效

四、双阻塞队列的生产消费模型

1.编写类代码

2.增加处理保存任务的函数

3.更改线程函数

4.更改main函数


一、线程同步

1.生产消费模型(或生产者消费者模型)

我们肯定有在超市买东西的经历,比如买水。超市的瓶装水是供应商提供的,所以供应商是生产者;超市从供应商进货,超市就是一个交易场所;我们从超市买水,我们就是消费者。

这些概念也可以转换到线程中,我们将读取数据的线程叫做消费者线程(消费者),将产生数据的线程叫做生产者线程(供应商),将共享的特定数据结构叫做缓冲区(交易场所)。

超市中售卖的瓶装水品牌很多,所以供应商肯定不止一个,这些不同牌子瓶装水的生产者之间的关系就是竞争关系。所以,多线程中各个生产者线程之间是互斥关系,同一时刻只有一个生产者线程能访问缓冲区。

对于消费者线程也是一样的,一个消费者买到了水,其他人可能就买不到了。所以消费者线程和消费者线程之间也是互斥关系,同一时刻也只有一个消费者线程能访问缓冲区。

超市也总要有补货的时间,如果当前仓库里有货,但是工作人员还没有将货物摆在货架上,那到来的顾客就以为这里没有水。

为了避免出问题,应当在超市补货时阻止消费者进入。由于缓冲区数据又被错误覆盖的风险,所以最好在消费者线程访问缓冲区时,不允许生产者线程访问缓冲区,反之亦然。

消费者线程和生产者线程之间也是互斥关系,在同一时间内只有一个线程可以访问缓冲区。

2.认识同步

(1)生产消费模型中的同步

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

首先什么叫饥饿状态?

我以多线程抢票代码为例,如果每个线程抢完票后都没有进行其他处理动作时,第一个申请到锁的线程更容易申请到锁。最终,大部分票都被一个线程抢走。而其他线程竞争能力弱,缺乏调度,这些线程就处于饥饿状态。

那什么是同步?

而同步就是让所有线程按照一定顺序来抢票,尽可能做到人人有份,避免线程饥饿问题产生。

再次回到超市买水,供应商不能没完没了地向超市供货,一方面超市一直关着,消费者无法消费,另一方面,超市又不是四次元口袋,总是要装满的。

同样,消费者也不能没完没了地买水,一方面超市不关门,供货商不能进货,另一方面,瓶装水又肯定会卖完。

所以最好生产者先供货,货架摆满了就不进货了。消费者来买,当水卖完了再让供应商进货,让消费者和生产者协同起来。

所以,消费者线程和生产者线程之间也是同步关系。生产者线程和消费者线程需要按照一定顺序去访问缓冲区。

所以,我们可以将生产消费模型总结为321原则:3种关系、2种角色、1个交易场所。

  • 3种关系:生产者和生产者(互斥关系),消费者和消费者(互斥关系),生产者和消费者(互斥和同步关系)
  • 2种角色:生产者和消费者
  • 1个交易场所:一段特定结构的缓冲区

生产消费模型的运行本质就是321原则。

(2)生产者消费者模型的特点

对供货商而言,只需要给超市供大量的货即可,不用关心消费者什么时候来买。

对消费者而言,只需要直接去超市买方便面就行,不用等待方便面的生产运输。

对超市而言,只需要在水卖完时,告诉供货商进货,进完货后告诉消费者来买。

生产消费模型中消费者和生产者各自只需要关心自己所做的事情,生产与消费线程线程之间完全独立,在计算机科学的角度,我们称其实现了消费者线程和生产者线程之间的解耦。

我们大部分人在周一到周五都是上班上学,所以这些时间去超市买水的人会相对少,这个时候超市就可以适时多进货。而周末大家都放假了,去超市买水的人变多,因为之前进货也很多了,就不需要进货了。

就像上面所说的策略,生产消费模型解决了生产者线程和消费者线程忙闲不均的问题。

而超市作为交易场所,能够储存更多的物品,同样缓冲区也能储存更多的数据。如果消费者直接去找供货商,供货商一般都不会零售。纵使能够零售,直接去找生产者还要等待生成者完成商品生产,消耗时间成本高,效率低。

生产者消费者模型提高了了生产者线程和消费者线程的执行效率。

二、条件变量

1.认识条件变量

条件变量是用来描述某种临界资源是否就绪的一种数据化描述。

比如说存在一个共享的容器,生产者线程负责生产数据到容器内,消费者线程负责从容器中中读取数据。消费者线程发现容器为空时,就不应当去竞争锁,而是阻塞等待,直到生产者线程将数据生成到容器中。

要想让消费者线程等待,那就必须使用条件变量标识容器的状态,那么就需要用到条件变量。

那条件变量到底是什么呢?

假设超市的架子进货一次只放一瓶水,只有这瓶水被买走后,供货商才会进货。

此时又有很多消费者来买水,只有竞争能力强的消费者才能买到水,甚至他们会不停地买。竞争能力弱的消费者,买不到水。放在线程中也是一样的,竞争能力弱的消费者线程始终抢不到锁,产生了饥饿问题。

为了解决这个问题,超市的工作人员设置了一个柜台,所有消费者都在这里排队,有一瓶水摆上货架,工作人员就允许一个消费者进去买,没有水所有人就需要在外面等待。而如果消费者想买第二瓶,就只能重新排队。而这个柜台和工作人员就相当于条件变量。

多线程互斥访问临界资源时,为了让这些线程按一定顺序访问。通常会将这些线程都放在条件变量的等待队列中,当其他线程让条件变量符合线程的唤醒条件时,队列中的第一个线程就会去访问临界资源。

2.条件变量的使用

条件变量同样是一个类(pthread_cond_t),由POSIX线程库维护,使用的是POSIX标准。它也可以构造对象pthread_cond_t cond,cond就是条件变量的对象。

以下是条件变量的一些成员函数和使用代码:

int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr);

头文件:pthread.h

功能:初始化条件变量。

参数:pthread_cond_t *restrict cond表示需要被初始化的条件变量的地址,const pthread_condattr_t *restrict attr表示条件变量的属性,一般都为nullptr。

返回值:取消成功返回0,取消失败返回错误码。

int pthread_cond_destroy(pthread_cond_t *cond);

头文件:pthread.h

功能:销毁互斥条件变量。

参数:pthread_cond_t *cond表示需要被销毁的条件变量的地址。

返回值:销毁成功返回0,失败返回错误码。

pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

如果是全局或static修饰的条件变量,使用上面语句初始化。

int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);

头文件:pthread.h

功能:将调用该接口的线程放入传入的条件变量等待队列中。

参数:pthread_cond_t *restrict cond创建的条件变量地址。pthread_mutex_t *restrict mutex互斥锁的地址(为什么传锁以后会解释)。

返回值:放入等待队列成功返回0,失败返回错误码。

int pthread_cond_signal(pthread_cond_t *cond);

头文件:pthread.h

功能:由另一个线程(通常是主线程)唤醒指定条件变量等待队列中的一个线程。

参数:pthread_cond_t *cond表示需要唤醒的线程所在的等待队列的条件变量地址。

返回值:唤醒成功返回0,失败返回错误码。

int pthread_cond_broadcast(pthread_cond_t *cond);

头文件:pthread.h

功能:由另一个线程(通常是主线程)唤醒指定条件变量等待队列中的所有线程。

参数:pthread_cond_t *cond表示需要唤醒的线程所在的等待队列的条件变量地址。

返回值:唤醒成功返回0,失败返回错误码。

3.代码改造

我们使用条件变量使所有进程可以以一定顺序抢票。

#include<iostream>
#include<pthread.h>
#include<unistd.h>
#include<stdio.h>
#include<vector>
using namespace std;

#define NUM 5

pthread_mutex_t mutx = PTHREAD_MUTEX_INITIALIZER;//构建一个全局锁
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;//构建一个全局条件变量
int tickets = 10;

class pthread_data
{
public:
    pthread_t tid;
    char buffer[64];
};

void* start_routine(void* args)
{
    pthread_data* p = (pthread_data*)args;
    string s;
    s += p->buffer;
    s += "Remaining tickets:";
    while(1)
    {
        pthread_mutex_lock(&mutx);//加锁
        pthread_cond_wait(&cond, &mutx);//线程进入等待队列
        if(tickets > 0)
        {
            --tickets;
            pthread_mutex_unlock(&mutx);//解锁
            printf("%s%d\n", s.c_str(), tickets);//不修改临界资源,可以不包含在内
        }
        else
        {
            pthread_mutex_unlock(&mutx);//解锁
            break;
        }
    }
    pthread_exit(nullptr);
}

int main()
{
    vector<pthread_data*> vpd;
    //创建多个线程
    for(int i = 0; i<NUM; ++i)
    {
        pthread_data* pd = new pthread_data;
        snprintf(pd->buffer, sizeof(pd->buffer), "thread%d buy ticket:",i+1);
        pthread_create(&(pd->tid), nullptr, start_routine, (void*)pd);
        vpd.push_back(pd);
    }
    //主线程唤醒其他线程
    for(;;)
    {
        sleep(1);
        pthread_cond_signal(&cond);
        printf("main thread wake up a thread\n");
    }
    //线程回收
    for(int i = 0; i<NUM; ++i)
    {
        pthread_join(vpd[i]->tid, nullptr);
        delete vpd[i];
    }
    return 0;
}

条件变量、票数和锁都是全局变量,每个线程申请锁成功就进入条件变量等待队列。主线程每个一秒钟唤醒一个等待的线程抢票。

运行结果:

可以发现线程按12345的顺序循环抢票。

使用pthread_cond_broadcast()接口可以一次唤醒条件变量等待队列中的的所有线程,每隔一秒唤醒一次。

运行结果:

仍然是按照一定顺序抢票,只是进行抢票的线程是5个同时进行。

三、基于阻塞队列的生产消费模型

既然讲了这么半天的生产消费模型,那我们不妨实现一个。

1.阻塞队列类

首先需要搭建模型的框架,也就是实现包括生产者线程、消费者线程,还有一个储存数据的阻塞队列(缓冲区)的简单执行代码。

(1)阻塞队列

阻塞队列的实现有以下注意事项:

阻塞队列可使用C++STL中的queue实现。

由于阻塞队列是公共资源,所以必须保证它是线程安全的。生产者线程和消费者线程需要互斥访问,其实也只需要一把互斥锁就能实现生产者和消费者间的互斥。以后的生产者和生产者,消费者和消费者之间的互斥也是这样实现的。

只有阻塞队列中有数据消费者才能读取,消费者读取时,生产者不能生产,必须在等待队列中。

阻塞队列中没有数据或者数据未填满时,生产者才能生产,消费者在生产的时候,消费者不能读取,必须在等待队列中。

template<claas T>
class Blockqueue
{
public:
    //构造函数
    Blockqueue(size_t capcity = MAX_NUM)
        :_capcity(capcity)
    {
        pthread_mutex_init(&_mutx, nullptr);
        pthread_cond_init(&_pcond, nullptr);
        pthread_cond_init(&_ccond, nullptr);
    }

    //析构函数
    ~Blockqueue()
    {
        pthread_mutex_destroy(&_mutx);
        pthread_cond_destroy(&_pcond);
        pthread_cond_destroy(&_ccond);
    }

    //生产数据
    void push(const T& data);
    //消费数据
    void pop(T* data);
private:
    //检测队列是否装满
    size_t Isfull() const
    {
        return (_q.size() == _capcity);
    }

    std::queue<T> _q;
    pthread_mutex_t _mutx;
    pthread_cond_t _pcond;
    pthread_cond_t _ccond;
    size_t _capcity;
};

为了保持生产者和消费者的互斥,我们对生产者和消费者各使用一个条件变量,用一个锁控制对阻塞队列的访问。

需要给阻塞队列储存的数据量设置一个上限,生产者线程不能无限制地生产数据。

构造函数中初始化锁和条件变量,在析构函数中释放互斥锁和条件变量。阻塞队列的容量设置一个合适的缺省值。

(2)实现生产者的生产函数

生产数据我们一般使用push作为函数名。

void push(const T& data)
{
    //下面的判断就开始使用共享资源,需要加锁
    pthread_mutex_lock(&_mutx);
       
    //如果当前队列是满的,那就需要将生产者线程加入等待队列挂起
    if(Isfull())
    {
        pthread_cond_wait(&_pcond, &_mutx)
    }
    _q.push(data);
    //唤醒消费者线程消费
    pthread_cond_signal(&_ccond, &_mutx);
    //解锁
    pthread_mutex_unlock(&_mutx);
}

生产者线程调用生产数据接口时,先申请锁进入临界区。

当阻塞队列满时,在生产者条件变量将线程放入等待队列中挂起。

当阻塞队列不满时,生产者生产数据到阻塞队列,由于消费者线程全部在等待,所以需要唤醒消费者线程消费数据,否则生产者会一直生产至满。

(3)实现消费者的消费函数

消费数据我们一般使用pop作为函数名。

void pop(T* data)
{
    //下面的判断就开始使用共享资源,需要加锁
    pthread_mutex_lock(&_mutx);
    //如果当前队列是空的,那就需要将消费者线程加入等待队列挂起
    if(_q.empty())
    {
        pthread_cond_wait(&_ccond, &_mutx)
    }
    //将数据输出到data中并删除
    *data = _q.front();
    _q.pop();
    //唤醒生产者线程生产
    pthread_cond_signal(&_pcond, &_mutx);
    //解锁
    pthread_mutex_unlock(&_mutx);
}

消费者线程同样先申请到锁后进入临界区。

当阻塞队列为空时,没有数据可以消费,消费者挂起等待。

当阻塞队列为不为空时,消费者消费数据,由生产者线程全部在等待,所以需要唤醒生产者线程消费数据,否则消费者会一直消费至空。

2.pthread_cond_wait为什么要传入锁

使用pthread_cond_wait接口时,必须传如一个锁,这一点我们没有解释。

线程在条件变量的等待队列中排队等待,其目的就是要拿到要访问临界资源的那把锁,申请到锁,线程就可以进入临界区。

如果一个线程拿到了锁,而又发现自己不满足条件需要挂起等待。按照之前的知识,该线程应该继续拿着锁进入条件变量的等待队列。即使其他线程被唤醒了,因为申请不到到锁,无法访问共享资源,只能被挂起。

为了解决这个问题,pthread_cond_wiat的实现大致分三个步骤:挂起该线程->释放锁->记录锁。

也就是说,只要是持有锁的线程进入等待队列,就自动释放自己持有的锁,而释放锁也是原子性操作,不会引起线程安全问题。

在最后,接口还会记录当前进程挂起时释放的锁,这也就解释了为什么唤醒线程的时候,pthread_cond_signal(pthread_cond_t* cond)只有一个参数,被唤醒线程根据记录就知道它该去申请哪把锁。

我们最终得到以下结论:

  • 参数传递的这个锁必须是正在使用的锁。
  • 调用pthread_cond_wait函数的进程,会以原子性的方式,将锁释放,并将自己挂起。
  • 线程在被唤醒的时候,会自动重新获取挂起时传入的锁。

3.生产者和消费者线程的执行函数

(1)执行函数

生产者不断生成随机数再将数据插入阻塞队列。消费者不断将随机数再从阻塞队列中拿出来。

//生产者
void* Produce(void* args)
{
    Blockqueue<int>* bq = (Blockqueue<int>*)args;
    while(1)
    {
        sleep(1);
        int data = rand()%10;
        bq->push(data);
        printf("生产数据完成,数据为:%d\n", data);
    }
    return nullptr;
}

//消费者
void* Consume(void* args)
{
    Blockqueue<int>* bq = (Blockqueue<int>*)args;
    while(1)
    {
        sleep(1);
        int data = 0;
        bq->pop(&data);
        printf("消费数据完成,数据为:%d\n", data);
    }
    return nullptr;
}

(2)试运行

我们只创建一个生产者线程和一个消费者线程,并且让生产者每一秒生产一个数据,所以最终代码如下:

#include<iostream>
#include<queue>
#include<stdlib.h>
#include<time.h>
#include<unistd.h>
using namespace std;

#define MAX_NUM 10

template<class T>
class Blockqueue
{
public:
    //构造函数
    Blockqueue(size_t capcity = MAX_NUM)
        :_capcity(capcity)
    {
        pthread_mutex_init(&_mutx, nullptr);
        pthread_cond_init(&_pcond, nullptr);
        pthread_cond_init(&_ccond, nullptr);
    }

    //析构函数
    ~Blockqueue()
    {
        pthread_mutex_destroy(&_mutx);
        pthread_cond_destroy(&_pcond);
        pthread_cond_destroy(&_ccond);
    }

    //生产数据
    void push(const T& data)
    {
        //下面的判断就开始使用共享资源,需要加锁
        pthread_mutex_lock(&_mutx);
        //如果当前队列是满的,那就需要将生产者线程加入等待队列挂起
        if(Isfull())
        {
            pthread_cond_wait(&_pcond, &_mutx);
        }
        _q.push(data);
        //唤醒消费者线程消费
        pthread_cond_signal(&_ccond);
        //解锁
        pthread_mutex_unlock(&_mutx);
    }

    //消费数据
    void pop(T* data)
    {
        //下面的判断就开始使用共享资源,需要加锁
        pthread_mutex_lock(&_mutx);
        //如果当前队列是空的,那就需要将消费者线程加入等待队列挂起
        if(_q.empty())
        {
            pthread_cond_wait(&_ccond, &_mutx);
        }
        //将数据输出到data中并删除
        *data = _q.front();
        _q.pop();
        //唤醒生产者线程生产
        pthread_cond_signal(&_pcond);
        //解锁
        pthread_mutex_unlock(&_mutx);
    }

private:
    //检测队列是否装满
    size_t Isfull() const
    {
        return (_q.size() == _capcity);
    }

    std::queue<T> _q;
    pthread_mutex_t _mutx;
    pthread_cond_t _pcond;
    pthread_cond_t _ccond;
    size_t _capcity;
};

//生产者
void* Produce(void* args)
{
    Blockqueue<int>* bq = (Blockqueue<int>*)args;
    while(1)
    {
        sleep(1);
        int data = rand()%10;
        bq->push(data);
        printf("生产数据完成,数据为:%d\n", data);
    }
    return nullptr;
}

//消费者
void* Consume(void* args)
{
    Blockqueue<int>* bq = (Blockqueue<int>*)args;
    while(1)
    {
        //sleep(1);
        int data = 0;
        bq->pop(&data);
        printf("消费数据完成,数据为:%d\n", data);
    }
    return nullptr;
}

int main()
{
    srand((unsigned int)time(nullptr));
    Blockqueue<int>* bq = new Blockqueue<int>;

    pthread_t tids[2];

    pthread_create(&tids[0], nullptr, Produce, (void*)bq);
    pthread_create(&tids[1], nullptr, Consume, (void*)bq);

    pthread_join(tids[0], nullptr);
    pthread_join(tids[1], nullptr);

    return 0;
}

运行结果:

4.部分细节处理

对于一个消费者和一个生产者的模型而言,上面的代码的确足够了。但是生产消费模型的生产者和消费者都应当有多个,此时我们就需要对其进行修改。

(1)伪唤醒问题

如果现在有多个生产者线程在进行数据生产。当阻塞队列满了以后,所有生产者线程都会在条件变量的等待队列中等待。

第一种情况,某个生产者线程调用挂起接口失败。

pthread_cond_wait即使调用失败,它也只会返回错误码,并不能阻断执行流继续向下执行。所以,即使出错,生产者还是会生成数据到阻塞队列中。

第二种情况,一个消费者线程一次唤醒所有生产者线程。

比如,现在阻塞队列满了,消费者线程消费了一个数据并且唤醒了所有的生产者线程,那么很多个生产者向阻塞队列的一个空位置生成数据,同样会出现上述问题。

上面这种情况被叫做伪唤醒,再生产者和消费者线程中都存在这个问题。所以需要让执行流只要不满足队列满或空的条件就循环执行pthread_cond_wait。执行成功了,该线程就被挂起了,执行流不再运行;执行失败了,执行流会一直调用pthread_cond_wait,执行流也不会向下操作数据。

生产数据

消费数据

(2)解锁与唤醒的顺序

在生产者生产完数据后,它需要做两件事:唤醒消费者线程和归还锁。

对于消费者进程也是一样的:唤醒生产者线程和归还锁。

由于唤醒线程是不对共享资源进行操作的,所以对于唤醒线程和解锁的顺序谁先谁后都可以。只是更建议先唤醒再解锁。

5.处理任务的生产消费模型

(1)代码改造

我们使用生产消费模型可不是用来保存随机数的,而是用它处理任务的。

我们可以写一个保存计算方法的任务类,从而实现一个随机数计算器。

任务类

//任务类
class Task
{
    typedef std::function<int(int,int,char)> func_t;
public:
    //默认构造
    Task()
    {}
    //构造函数
    Task(int a, int b, char op, func_t func)
        :_a(a)
        ,_b(b)
        ,_op(op)
        ,_func(func)
    {}

    //仿函数
    string operator()()
    {
        int result = _func(_a, _b, _op);
        char buffer[64];
        snprintf(buffer, sizeof(buffer), "%d %c %d = %d\n", _a, _op, _b, result);
        string s(buffer);
        return s;
    }

    //显示任务
    string show_task()
    {
        char buffer[64];
        snprintf(buffer, sizeof(buffer), "%d %c %d = ?\n", _a, _op, _b);
        string s(buffer);
        return s;
    }
private:
    func_t _func;
    int _a;
    int _b;
    char _op;
};

设置处理任务的函数,修改生产者和消费者线程执行的函数。

//计算器函数
const string ops = "+-*/%";
int calculate(int a, int b, char op)
{
    int result = 0;
    switch(op)
    {
        case '+':
            result = a + b;
            break;
        case '-':
            result = a - b;
            break;
        case '*':
            result = a * b;
            break;
        case '/':
        {
            if(b == 0)
                cerr << "除数不能为0\n";
            else
                result = a / b;
        }
            break;
        case '%':
        {
            if(b == 0)
                cerr << "取模的数字不能为0\n";
            else
                result = a % b;
        }
            break;
        default:
            break;
    }
    return result;
}

//生产者
void* Produce(void* args)
{
    Blockqueue<Task>* bq = (Blockqueue<Task>*)args;
    while(1)
    {
        sleep(1);
        int a = rand()%10;
        int b = rand()%10;
        int opnum = rand()%ops.size();
        Task data(a, b, ops[opnum], calculate);
        string s = "数据生产完成,需要计算:";
        bq->push(data);
        s += data.show_task().c_str();
        cout << s;
    }
    return nullptr;
}

(2)运行

在主线程中多创建几个线程就可以运行了。

总代码如下:

#include<iostream>
#include<queue>
#include<stdlib.h>
#include<time.h>
#include<unistd.h>
#include<functional>
using namespace std;

#define MAX_NUM 10

//任务类
class Task
{
    typedef std::function<int(int,int,char)> func_t;
public:
    //默认构造
    Task()
    {}
    //构造函数
    Task(int a, int b, char op, func_t func)
        :_a(a)
        ,_b(b)
        ,_op(op)
        ,_func(func)
    {}

    //仿函数
    string operator()()
    {
        int result = _func(_a, _b, _op);
        char buffer[64];
        snprintf(buffer, sizeof(buffer), "%d %c %d = %d\n", _a, _op, _b, result);
        string s(buffer);
        return s;
    }

    //显示任务
    string show_task()
    {
        char buffer[64];
        snprintf(buffer, sizeof(buffer), "%d %c %d = ?\n", _a, _op, _b);
        string s(buffer);
        return s;
    }
private:
    func_t _func;
    int _a;
    int _b;
    char _op;
};

template<class T>
class Blockqueue
{
public:
    //构造函数
    Blockqueue(size_t capcity = MAX_NUM)
        :_capcity(capcity)
    {
        pthread_mutex_init(&_mutx, nullptr);
        pthread_cond_init(&_pcond, nullptr);
        pthread_cond_init(&_ccond, nullptr);
    }

    //析构函数
    ~Blockqueue()
    {
        pthread_mutex_destroy(&_mutx);
        pthread_cond_destroy(&_pcond);
        pthread_cond_destroy(&_ccond);
    }

    //生产数据
    void push(const T& data)
    {
        //下面的判断就开始使用共享资源,需要加锁
        pthread_mutex_lock(&_mutx);
        //如果当前队列是满的,那就需要将生产者线程加入等待队列挂起
        while(Isfull())
        {
            pthread_cond_wait(&_pcond, &_mutx);
        }
        _q.push(data);
        //唤醒消费者线程消费
        pthread_cond_signal(&_ccond);
        //解锁
        pthread_mutex_unlock(&_mutx);
    }

    //消费数据
    void pop(T* data)
    {
        //下面的判断就开始使用共享资源,需要加锁
        pthread_mutex_lock(&_mutx);
        //如果当前队列是空的,那就需要将消费者线程加入等待队列挂起
        while(_q.empty())
        {
            pthread_cond_wait(&_ccond, &_mutx);
        }
        //将数据输出到data中并删除
        *data = _q.front();
        _q.pop();
        //唤醒生产者线程生产
        pthread_cond_signal(&_pcond);
        //解锁
        pthread_mutex_unlock(&_mutx);
    }

private:
    //检测队列是否装满
    size_t Isfull() const
    {
        return (_q.size() == _capcity);
    }

    std::queue<T> _q;
    pthread_mutex_t _mutx;
    pthread_cond_t _pcond;
    pthread_cond_t _ccond;
    size_t _capcity;
};

//计算器函数
const string ops = "+-*/%";
int calculate(int a, int b, char op)
{
    int result = 0;
    switch(op)
    {
        case '+':
            result = a + b;
            break;
        case '-':
            result = a - b;
            break;
        case '*':
            result = a * b;
            break;
        case '/':
        {
            if(b == 0)
                cerr << "除数不能为0\n";
            else
                result = a / b;
        }
            break;
        case '%':
        {
            if(b == 0)
                cerr << "取模的数字不能为0\n";
            else
                result = a % b;
        }
            break;
        default:
            break;
    }
    return result;
}

//生产者
void* Produce(void* args)
{
    Blockqueue<Task>* bq = (Blockqueue<Task>*)args;
    while(1)
    {
        sleep(1);
        int a = rand()%10;
        int b = rand()%10;
        int opnum = rand()%ops.size();
        Task data(a, b, ops[opnum], calculate);
        string s = "数据生产完成,需要计算:";
        bq->push(data);
        s += data.show_task().c_str();
        cout << s;
    }
    return nullptr;
}

//消费者
void* Consume(void* args)
{
    Blockqueue<Task>* bq = (Blockqueue<Task>*)args;
    while(1)
    {
        //sleep(1);
        Task data;
        string s = "数据消费完成,计算结果为:";
        bq->pop(&data);
        string result = data();
        s += result;
        cout << s;
    }
    return nullptr;
}

#define NUM_PRODUCE 3
#define NUM_CONSUME 3

int main()
{
    srand((unsigned int)time(nullptr));
    Blockqueue<Task>* bq = new Blockqueue<Task>;

    pthread_t ptids[NUM_PRODUCE];
    pthread_t ctids[NUM_CONSUME];

    //创建多个生产者线程
    for(int i = 0; i<NUM_PRODUCE; ++i)
    {
        pthread_create(&ptids[i], nullptr, Produce, (void*)bq);
    }

    //创建多个消费者线程
    for(int i = 0; i<NUM_CONSUME; ++i)
    {
        pthread_create(&ctids[i], nullptr, Consume, (void*)bq);
    }

    //回收所有线程
    for(int i = 0; i<NUM_PRODUCE; ++i)
    {
        pthread_join(ptids[i], nullptr);
    }
    for(int i = 0; i<NUM_CONSUME; ++i)
    {
        pthread_join(ctids[i], nullptr);
    }

    return 0;
}

运行结果:

有一个地方我一直没说,像cout 所以我们在打印信息时尽量使用单句printf或者cout

6.生产消费模型为何高效

该模型中,多个生产者线程向阻塞队列生成数据,多个消费者线程也从阻塞队列中消费数据。

各生产消费者线程之间互斥关系,各线程对于阻塞队列的访问是串行的。同一时间访问阻塞队列的线程只有一个,拿这样又何来高效呢?

注意观察程序的运行逻辑,我们能发现只有临界区的代码是串行的,其他代码所有线程都是并发执行的。这些非临界区的代码通常耗时长,而它们是并发的,所以该模型的效率就变得很高。

结论:生产消费模型的高效不体现在对临界资源的访问上,而是体现在对非临界区代码的并发执行上。

四、双阻塞队列的生产消费模型

我么们可以使用上面的生产者消费者模型,将消费者处理完的计算任务保存成日志,并储存到磁盘上。

所以该模型有两个阻塞队列,一个阻塞队列用于保存计算任务,另一个阻塞队列用于保存保存任务。原来的生产者线程还是生产者,原来的消费者作为中间的线程,既是消费者也是生产者,保存线程是消费者。

1.编写类代码

我们需要构建四个类:计算任务类、保存任务类、阻塞队列类和多队列集合类。

  • 计算任务类就是之前的Task类,我们其他代码不用动,对它改个CalTask的名字就可以。
  • 保存任务类与计算任务类很相似,需要传入一个string表示需要打印的信息,还有一个函数对象表示保存数据的具体执行函数。
  • 阻塞队列类也不用改
  • 多队列集合类是为了线程执行函数的void* args传参设计的,包含了两个阻塞队列指针。
//计算任务类
class CalTask
{
    typedef std::function<int(int,int,char)> func_t;
public:
    //默认构造
    CalTask()
    {}
    //构造函数
    CalTask(int a, int b, char op, func_t func)
        :_a(a)
        ,_b(b)
        ,_op(op)
        ,_func(func)
    {}

    //仿函数
    string operator()()
    {
        int result = _func(_a, _b, _op);
        char buffer[64];
        snprintf(buffer, sizeof(buffer), "%d %c %d = %d\n", _a, _op, _b, result);
        string s(buffer);
        return s;
    }

    //显示任务
    string show_task()
    {
        char buffer[64];
        snprintf(buffer, sizeof(buffer), "%d %c %d = ?\n", _a, _op, _b);
        string s(buffer);
        return s;
    }
private:
    func_t _func;
    int _a;
    int _b;
    char _op;
};

//保存任务类
class SaveTask
{
    typedef function<void(const string&)> func_t;
public:
    //默认构造
    SaveTask()
    {}
    //构造函数
    SaveTask(string message, func_t func)
        :_message(message)
        ,_func(func)
    {}

    //仿函数
    void operator()()
    {
        _func(_message);
    }
private:
    string _message;
    func_t _func;
};

//阻塞队列类
template<class T>
class Blockqueue
{
public:
    //构造函数
    Blockqueue(size_t capcity = MAX_NUM)
        :_capcity(capcity)
    {
        pthread_mutex_init(&_mutx, nullptr);
        pthread_cond_init(&_pcond, nullptr);
        pthread_cond_init(&_ccond, nullptr);
    }

    //析构函数
    ~Blockqueue()
    {
        pthread_mutex_destroy(&_mutx);
        pthread_cond_destroy(&_pcond);
        pthread_cond_destroy(&_ccond);
    }

    //生产数据
    void push(const T& data)
    {
        //下面的判断就开始使用共享资源,需要加锁
        pthread_mutex_lock(&_mutx);
        //如果当前队列是满的,那就需要将生产者线程加入等待队列挂起
        while(Isfull())
        {
            pthread_cond_wait(&_pcond, &_mutx);
        }
        _q.push(data);
        //唤醒消费者线程消费
        pthread_cond_signal(&_ccond);
        //解锁
        pthread_mutex_unlock(&_mutx);
    }

    //消费数据
    void pop(T* data)
    {
        //下面的判断就开始使用共享资源,需要加锁
        pthread_mutex_lock(&_mutx);
        //如果当前队列是空的,那就需要将消费者线程加入等待队列挂起
        while(_q.empty())
        {
            pthread_cond_wait(&_ccond, &_mutx);
        }
        //将数据输出到data中并删除
        *data = _q.front();
        _q.pop();
        //唤醒生产者线程生产
        pthread_cond_signal(&_pcond);
        //解锁
        pthread_mutex_unlock(&_mutx);
    }

private:
    //检测队列是否装满
    size_t Isfull() const
    {
        return (_q.size() == _capcity);
    }

    std::queue<T> _q;
    pthread_mutex_t _mutx;
    pthread_cond_t _pcond;
    pthread_cond_t _ccond;
    size_t _capcity;
};

//多队列集合类
template<class A,class B>
class Blockqueues
{
public:
    Blockqueue<A>* _q1;
    Blockqueue<B>* _q2;
};

2.增加处理保存任务的函数

我们之前对计算任务有一个处理函数calculate,那处理保存任务也同样需要一个函数Savedata。

//保存函数
void Savedata(const string& message)
{
    //需要保存信息的文件
    char buffer[64] = "log.txt";
    //按追加方式打开文件
    FILE* fp = fopen(buffer, "a");
    if(fp == nullptr)
    {
        cerr << "文件打开失败" << endl;
        return;
    }
    fprintf(fp, "%s", message.c_str());
    fclose(fp);
}

3.更改线程函数

生产者线程此时依旧生产,只不过生产的位置是第一个阻塞队列。

消费者线程此时也成为了后一个模型的生产者,那就需要添加向第二个队列生产的代码。

保存者线程此时是消费者,从第二个队列中取任务执行。

//生产者
void* Produce(void* args)
{
    Blockqueues<CalTask, SaveTask>* bqs = (Blockqueues<CalTask, SaveTask>*)args;
    //生产数据
    while(1)
    {
        sleep(1);
        int a = rand()%10;
        int b = rand()%10;
        int opnum = rand()%ops.size();
        CalTask data(a, b, ops[opnum], calculate);
        string s = "数据生产完成,需要计算:";
        bqs->_q1->push(data);
        s += data.show_task().c_str();
        cout << s;
    }
    return nullptr;
}

//消费者
void* Consume(void* args)
{
    Blockqueues<CalTask, SaveTask>* bqs = (Blockqueues<CalTask, SaveTask>*)args;
    while(1)
    {
        //消费数据
        //sleep(1);
        CalTask data;
        string s1 = "数据消费完成,计算结果为:";
        bqs->_q1->pop(&data);
        string result = data();
        s1 += result;
        cout << s1;
        //生成待保存的数据
        string s2 = "数据保存任务推送完毕\n";
        SaveTask task = SaveTask(result, Savedata);
        bqs->_q2->push(task);
         cout << s2;
    }
    return nullptr;
}

void* Save(void* args)
{
    Blockqueues<CalTask, SaveTask>* bqs = (Blockqueues<CalTask, SaveTask>*)args;
    while(1)
    {
        //sleep(1);
        SaveTask data;
        string s = "数据保存完成\n";
        bqs->_q2->pop(&data);
        data();
        cout << s;
    }
    return nullptr;
}

4.更改main函数

最后我们在main函数中创建两个队列,创建三种线程最后加上回收代码就可以了。

所以总代码如下:

produce_consume.h

#include<iostream>
#include<queue>
#include<stdlib.h>
#include<time.h>
#include<unistd.h>
#include<functional>
#include<stdio.h>
#define MAX_NUM 10
using namespace std;
//计算任务类
class CalTask
{
    typedef std::function<int(int,int,char)> func_t;
public:
    //默认构造
    CalTask()
    {}
    //构造函数
    CalTask(int a, int b, char op, func_t func)
        :_a(a)
        ,_b(b)
        ,_op(op)
        ,_func(func)
    {}

    //仿函数
    string operator()()
    {
        int result = _func(_a, _b, _op);
        char buffer[64];
        snprintf(buffer, sizeof(buffer), "%d %c %d = %d\n", _a, _op, _b, result);
        string s(buffer);
        return s;
    }

    //显示任务
    string show_task()
    {
        char buffer[64];
        snprintf(buffer, sizeof(buffer), "%d %c %d = ?\n", _a, _op, _b);
        string s(buffer);
        return s;
    }
private:
    func_t _func;
    int _a;
    int _b;
    char _op;
};

//保存任务类
class SaveTask
{
    typedef function<void(const string&)> func_t;
public:
    //默认构造
    SaveTask()
    {}
    //构造函数
    SaveTask(string message, func_t func)
        :_message(message)
        ,_func(func)
    {}

    //仿函数
    void operator()()
    {
        _func(_message);
    }
private:
    string _message;
    func_t _func;
};

//阻塞队列类
template<class T>
class Blockqueue
{
public:
    //构造函数
    Blockqueue(size_t capcity = MAX_NUM)
        :_capcity(capcity)
    {
        pthread_mutex_init(&_mutx, nullptr);
        pthread_cond_init(&_pcond, nullptr);
        pthread_cond_init(&_ccond, nullptr);
    }

    //析构函数
    ~Blockqueue()
    {
        pthread_mutex_destroy(&_mutx);
        pthread_cond_destroy(&_pcond);
        pthread_cond_destroy(&_ccond);
    }

    //生产数据
    void push(const T& data)
    {
        //下面的判断就开始使用共享资源,需要加锁
        pthread_mutex_lock(&_mutx);
        //如果当前队列是满的,那就需要将生产者线程加入等待队列挂起
        while(Isfull())
        {
            pthread_cond_wait(&_pcond, &_mutx);
        }
        _q.push(data);
        //唤醒消费者线程消费
        pthread_cond_signal(&_ccond);
        //解锁
        pthread_mutex_unlock(&_mutx);
    }

    //消费数据
    void pop(T* data)
    {
        //下面的判断就开始使用共享资源,需要加锁
        pthread_mutex_lock(&_mutx);
        //如果当前队列是空的,那就需要将消费者线程加入等待队列挂起
        while(_q.empty())
        {
            pthread_cond_wait(&_ccond, &_mutx);
        }
        //将数据输出到data中并删除
        *data = _q.front();
        _q.pop();
        //唤醒生产者线程生产
        pthread_cond_signal(&_pcond);
        //解锁
        pthread_mutex_unlock(&_mutx);
    }

private:
    //检测队列是否装满
    size_t Isfull() const
    {
        return (_q.size() == _capcity);
    }

    std::queue<T> _q;
    pthread_mutex_t _mutx;
    pthread_cond_t _pcond;
    pthread_cond_t _ccond;
    size_t _capcity;
};

//多队列集合类
template<class A,class B>
class Blockqueues
{
public:
    Blockqueue<A>* _q1;
    Blockqueue<B>* _q2;
};

produce_consume.cc

#include"produce_consume.h"
using namespace std;

//计算器函数
const string ops = "+-*/%";
int calculate(int a, int b, char op)
{
    int result = 0;
    switch(op)
    {
        case '+':
            result = a + b;
            break;
        case '-':
            result = a - b;
            break;
        case '*':
            result = a * b;
            break;
        case '/':
        {
            if(b == 0)
                cerr << "除数不能为0\n";
            else
                result = a / b;
        }
            break;
        case '%':
        {
            if(b == 0)
                cerr << "取模的数字不能为0\n";
            else
                result = a % b;
        }
            break;
        default:
            break;
    }
    return result;
}

//保存函数
void Savedata(const string& message)
{
    //需要保存信息的文件
    char buffer[64] = "log.txt";
    //按追加方式打开文件
    FILE* fp = fopen(buffer, "a");
    if(fp == nullptr)
    {
        cerr << "文件打开失败" << endl;
        return;
    }
    fprintf(fp, "%s", message.c_str());
    fclose(fp);
}

//生产者
void* Produce(void* args)
{
    Blockqueues<CalTask, SaveTask>* bqs = (Blockqueues<CalTask, SaveTask>*)args;
    //生产数据
    while(1)
    {
        sleep(1);
        int a = rand()%10;
        int b = rand()%10;
        int opnum = rand()%ops.size();
        CalTask data(a, b, ops[opnum], calculate);
        string s = "数据生产完成,需要计算:";
        bqs->_q1->push(data);
        s += data.show_task().c_str();
        cout << s;
    }
    return nullptr;
}

//消费者
void* Consume(void* args)
{
    Blockqueues<CalTask, SaveTask>* bqs = (Blockqueues<CalTask, SaveTask>*)args;
    while(1)
    {
        //消费数据
        //sleep(1);
        CalTask data;
        string s1 = "数据消费完成,计算结果为:";
        bqs->_q1->pop(&data);
        string result = data();
        s1 += result;
        cout << s1;
        //生成待保存的数据
        string s2 = "数据保存任务推送完毕\n";
        SaveTask task = SaveTask(result, Savedata);
        bqs->_q2->push(task);
         cout << s2;
    }
    return nullptr;
}

void* Save(void* args)
{
    Blockqueues<CalTask, SaveTask>* bqs = (Blockqueues<CalTask, SaveTask>*)args;
    while(1)
    {
        //sleep(1);
        SaveTask data;
        string s = "数据保存完成\n";
        bqs->_q2->pop(&data);
        data();
        cout << s;
    }
    return nullptr;
}

#define NUM_PRODUCE 3
#define NUM_CONSUME 3
#define NUM_SAVE 3

int main()
{
    srand((unsigned int)time(nullptr));
    Blockqueues<CalTask, SaveTask>* bqs = new Blockqueues<CalTask, SaveTask>();
    bqs->_q1 = new Blockqueue<CalTask>();
    bqs->_q2 = new Blockqueue<SaveTask>();

    pthread_t ptids[NUM_PRODUCE];
    pthread_t ctids[NUM_CONSUME];
    pthread_t stids[NUM_SAVE];

    //创建多个生产者线程
    for(int i = 0; i<NUM_PRODUCE; ++i)
    {
        pthread_create(&ptids[i], nullptr, Produce, (void*)bqs);
    }

    //创建多个消费者线程
    for(int i = 0; i<NUM_CONSUME; ++i)
    {
        pthread_create(&ctids[i], nullptr, Consume, (void*)bqs);
    }

    //创建多个保存者线程
    for(int i = 0; i<NUM_CONSUME; ++i)
    {
        pthread_create(&stids[i], nullptr, Save, (void*)bqs);
    }

    //回收所有线程
    for(int i = 0; i<NUM_PRODUCE; ++i)
    {
        pthread_join(ptids[i], nullptr);
    }
    for(int i = 0; i<NUM_CONSUME; ++i)
    {
        pthread_join(ctids[i], nullptr);
    }
    for(int i = 0; i<NUM_SAVE; ++i)
    {
        pthread_join(stids[i], nullptr);
    }
    return 0;
}

运行结果:

目录中确实多了一个log.txt,也能看出线程的具体执行轨迹。

基于阻塞队列的生产者消费者模型是线程同步与互斥的充分应用,现实中很多场景都可以应用,是线程中的一大杀器。

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

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

相关文章

B092-人力资源项目-security

目录 springsecurity权限控制使用的必要性分析及它的概念介绍基于session的认证和授权流程介绍认证流程认证检查授权流程 代码认证流程小结认证授权流程Security中核心过滤器链security执行认证的详细流程图Security授权流程剩余见代码工程 springsecurity权限控制使用的必要性…

2、Nginx 安装

文章目录 2、Nginx 安装2.1 官网下载2.2 安装 nginx2.2.1 第一步2.2.2 第二步2.2.3 第三步&#xff0c;安装 nginx2.2.4 第四步&#xff0c;修改防火漆规则 【尚硅谷】尚硅谷Nginx教程由浅入深 志不强者智不达&#xff1b;言不信者行不果。 2、Nginx 安装 2.1 官网下载 nginx…

vim练级攻略(精简版)

vim推荐配置: curl -sLf https://gitee.com/HGtz2222/VimForCpp/raw/master/install.sh -o ./install.sh && bash ./install.sh 0. 规定 Ctrl-λ 等价于 <C-λ> :command 等价于 :command <回车> n 等价于 数字 blank字符 等价于 空格&#xff0c;tab&am…

HuggingFace 简介

HuggingFace 简介 0. HuggingFace 简介1. HuggingFace 官网地址2. HuggingFace 标准研发流程3. HuggingFace 工具集4. 编码工具4.1 编码工具介绍4.2 使用编码工具 5. 数据集工具5.1 数据集工具介绍5.2 使用数据集工具 6. 评价指标工具6.1 评价指标工具介绍6.2 使用评价指标工具…

微信小程序 通过设置开发者工具编译模式 改变进入后的第一个page界面

在很多时候 我们小程序开发阶段&#xff0c;只需要写某个界面&#xff0c;嫌一级一级点进去太麻烦了 我们可以 打开开发者工具 选择自己正在开发的小程序 然后 上面选择编译模式(操作如下图) 然后 选择 添加编译模式 然后 弹出的配置栏中 重点是 启动页面 选择自己的page 然后…

友元(个人学习笔记黑马学习)

1、全局函数做友元 #include <iostream> using namespace std; #include <string>//建筑物类 class Building {//goodGay全局函数是 Building好朋友 可以访问Building中私有成员friend void goodGay(Building* building);public:Building() {m_SittingRoom "…

操作系统备考学习 day2 (1.3.2 - 1.6)

操作系统备考学习 day2 计算机系统概述操作系统运行环境中断和异常的概念系统调用 操作系统体系结构操作系统引导虚拟机 计算机系统概述 操作系统运行环境 中断和异常的概念 中断的作用 CPU上会运行两种程序&#xff0c;一种是操作系统内核程序&#xff0c;一种是应用程序。…

r 安装源码包 安装本地r包

总结一下手动安装R包 - 简书 (jianshu.com)https://www.jianshu.com/p/2a7a36414734 #BiocManager::install("simplifyEnrichment") #BiocManager::install("EnsDb.Hsapiens.v86")#下载包 之后 手动安装 #install.packages("~/datasets/EnsDb.Hsapien…

Grafana之魔法:揭秘数据可视化的艺术

在数据驱动的时代&#xff0c;如何有效地呈现和理解数据成为了每个组织和个人的核心任务。Grafana作为一个领先的开源数据可视化工具&#xff0c;为我们提供了强大的功能和灵活性。本文将深入探讨Grafana的魔法&#xff0c;以及它如何帮助我们更好地理解数据。 Grafana简介 G…

学习周报9.3

文章目录 前言文献阅读一摘要挑战基于时间序列的 GAN 分类 文献阅读二摘要介绍提出的模型:时间序列GAN (TimeGAN) 代码学习总结 前言 本周阅读两篇文献&#xff0c;文献一是一篇时序生成方面的综述&#xff0c;主要了解基于时间序列 的GAN主要分类以及时间序列GAN方面面临的一…

elasticsearch的索引库操作

索引库就类似数据库表&#xff0c;mapping映射就类似表的结构。我们要向es中存储数据&#xff0c;必须先创建“库”和“表”。 mapping映射属性 mapping是对索引库中文档的约束&#xff0c;常见的mapping属性包括&#xff1a; type&#xff1a;字段数据类型&#xff0c;常见的…

【个人博客系统网站】框架升级 · 工程目录 · 数据库设计

【JavaEE】进阶 个人博客系统&#xff08;1&#xff09; 文章目录 【JavaEE】进阶 个人博客系统&#xff08;1&#xff09;1. 使用Spring全家桶 MyBatis框架进行开发2. 页面2.1 登录页2.2 注册页2.3 详情页2.4 我的博客列表页3.5 所有人的博客列表页3.6 添加博客页3.7 修改文…

Snipaste_2023-08-22_16-09-41.jpg

原因 cd 目录名 (想要补全的时候出现以下错误) cd /o-bash: cannot create temp file for here-document: No space left on device解决方案 可以使用df -h命令查看磁盘空间的使用情况,删除一些不必要的文件或调整其他文件的存储位置来释放空间,或者还可以考虑扩大磁盘容量 df …

【java基础面试题】jdk、jre、jvm区别

【java基础面试题】jdk、jre、jvm区别 jdk ​ 从概念上讲JDK是JAVA开发工具,用它来开发JAVA程序&#xff0c;里面有很多基础类库和jre。 ​ JDK&#xff08;Java Development Kit&#xff09;&#xff0c;它是功能齐全的 Java SDK&#xff0c;是提供给开发者使用的&#xff…

[Linux]文件描述符(万字详解)

[Linux]文件描述符 文章目录 [Linux]文件描述符文件系统接口open函数close函数write函数read函数系统接口与编程语言库函数的关系 文件描述符文件描述符的概念文件数据交换的原理理解“一切皆文件”进程默认文件描述符文件描述符和编程语言的关系 重定向输出重定向输入重定向追…

RK3568 USB支持接口类型

一.简介 RK356x 总共支持 4 个 USB 外设接口&#xff0c;包括 1 个OTG 接口&#xff0c;1 个 USB 3.0 Host 接口&#xff0c;以及 2 个 USB 2.0 Host 接口。 二.常用接口类型介绍 Type-C 接口类型&#xff1a; Type-A 接口类型&#xff1a; Type-A USB 3.1 接口&#xff1a;…

el-tree组件的锚点链接

el-tree部分&#xff1a; <el-tree:default-expand-all"true":data"anchorList":props"defaultProps"node-click"handleNodeClick"/> 组件内部部分&#xff1a; <div class"header" :id"content obj.id&q…

javaweb入门版学生信息管理系统-增删改查+JSP+Jstl+El

dao public class StudentDao {QueryRunner queryRunner QueryRunnerUtils.getQueryRunner();//查询全部学生信息public List<Student> selectStudent(){String sql "select * from tb_student";List<Student> students null;try {students queryRunn…

机器学习笔记之最优化理论与方法(五)凸优化问题(上)

机器学习笔记之最优化理论与方法——凸优化问题[上] 引言凸优化问题的基本定义凸优化定义&#xff1a;示例 凸优化与非凸优化问题的区分局部最优解即全局最优解凸优化问题的最优性条件几种特殊凸问题的最优性条件无约束凸优化等式约束凸优化非负约束凸优化 引言 本节将介绍凸优…

Swift 技术 视频播放器滚动条(源码)

一直觉得自己写的不是技术&#xff0c;而是情怀&#xff0c;一个个的教程是自己这一路走来的痕迹。靠专业技能的成功是最具可复制性的&#xff0c;希望我的这条路能让你们少走弯路&#xff0c;希望我能帮你们抹去知识的蒙尘&#xff0c;希望我能帮你们理清知识的脉络&#xff0…