生产者-消费者模型

目录

生产者-消费者模型介绍

生产者-消费者模型优点

生产者-消费者之间的关系

基于阻塞队列实现生产者-消费者模型

基于环形队列实现生产者-消费者模型


生产者-消费者模型介绍

● 计算机中的生产者和消费者本质都是线程/进程

● 生产者和消费者不直接通讯,而是通过一段内存缓冲区进行通讯

● 生产者生产完数据直接放在内存缓冲区,不必等待消费者消费;消费者需要消费数据直接从内存缓冲区中取,不必等待生产者生产

● 生产者和消费者只有1个,是单生产-单消费模型;生产者和消费者有多个,是多生产-多消费模型

● 缓冲区满了,生产者会阻塞等待,不会再生产数据了,直到消费者消费了数据;缓冲区空了,消费者会阻塞等待,不会再消费数据了,直到生产者生产了数据

生产者-消费者模型优点

● 平衡消费者和生产者处理数据的能力,一方面起到缓存的作用,另一方面达到解耦合的效果

消费者和生产者数据数据的能力可能差别比较大,比如消费者消费速度很快,而生产者生产比较慢,那么消费者快速消费完数据后就会经常等待生产者生产,而引入一段内存缓冲区之后,可以缓存一定的数据,生产者/消费者直接使用这段内存缓冲区即可,一定程度上平衡消费者和生产者处理数据的能力,生产同时可以消费,消费同时也可以生产,一定程度上将生产者和消费者解耦,支持并发运行和忙闲不均

生产者-消费者之间的关系

● 生产者-生产者:互斥

● 消费者-消费者:互斥

● 生产者-消费者:互斥+同步

1. 多个生产者可能会同时生产数据,多个消费者也可能会同时消费数据,生产者和消费者也可以同时生产和消费数据,总结一下,同一时刻会有多个执行流访问内存缓冲区,因此内存缓冲区是一份公共资源,因此需要使用互斥锁保护起来,因此生产者-生产者,消费者-消费者,生产者-消费者都会竞争锁,属于互斥关系

2. 缓冲区满了,生产者会阻塞等待,不会再生产数据了,直到消费者消费了数据;缓冲区空了,消费者会阻塞等待,不会再消费数据了,直到生产者生产了数据,因此生产者和消费者呈现出一定的同步关系,需要让生产者线程和消费者线程协同起来

基于阻塞队列实现生产者-消费者模型

BlockQueue.hpp

#pragma once
#include <iostream>
#include <queue>
#include <pthread.h>
#include "LockGuard.hpp"

using namespace std;

const int defaultcap = 5;

template <class T>
class BlockQueue
{
public:
    BlockQueue(int cap = defaultcap)
        : _capacity(cap)
    {
        pthread_mutex_init(&_mutex, nullptr);
        pthread_cond_init(&_p_cond, nullptr);
        pthread_cond_init(&_c_cond, nullptr);
    }

    bool IsFull()
    {
        return _q.size() == _capacity;
    }

    bool IsEmpty()
    {
        return _q.size() == 0;
    }

    void Push(const T &in) // 生产者的
    {
        LockGuard lockguard(&_mutex);
        while (IsFull())
        {
            // 队列满了, 生产线程阻塞等待
            pthread_cond_wait(&_p_cond, &_mutex);
        }
        _q.push(in); // 将任务放到队列
        pthread_cond_signal(&_c_cond); // 只要生产了,就立即通知消费者可以消费了!
    }

    void Pop(T *out) // 消费者的
    {
        LockGuard lockguard(&_mutex);
        while (IsEmpty())
        {
            // 队列空了, 消费线程阻塞等待
            pthread_cond_wait(&_c_cond, &_mutex);
        }
        *out = _q.front(); // 从队列中拿数据
        _q.pop();
        pthread_cond_signal(&_p_cond); // 只要消费了,就立即通知生产者可以生产了!
    }

    ~BlockQueue()
    {
        pthread_mutex_destroy(&_mutex);
        pthread_cond_destroy(&_p_cond);
        pthread_cond_destroy(&_c_cond);
    }

private:
    queue<T> _q;            // 阻塞队列
    int _capacity;          // 容量 _q.size() == _capacity, 满了,不能再生产; _q.size() == 0, 空, 不能消费了
    pthread_mutex_t _mutex; // 互斥
    pthread_cond_t _p_cond; // 给生产者的, 生产条件不满足,就排队
    pthread_cond_t _c_cond; // 给消费者的, 消费条件不满足,就排队
};

● 使用STL中的队列模拟阻塞队列,只是额外限制了队列的容量上限

● 为了维护生产者和消费者之间的互斥+同步关系,引入锁和条件变量

● 队列为空,消费者不能再消费,需要在特定条件变量下等待,直到生产者生产了数据,通知消费者可以消费了(实际也可以制定其他策略,比如生产者生产的数据超过队列容量的1/3)

● 队列满了,生产者不能再生产,需要在特定条件变量下等待,直到消费者消费了数据,通知生产者可以生产了(实际也可以制定其他策略,比如消费者生产的数据超过队列容量的2/3)

● 判断生产条件是否满足以及消费条件是否满足时,我们使用的是while而不是if,是因为使用if可能会导致 pthread_cond_wait 出现伪唤醒状态

因为生产者可能是多个线程,如果队列满了,多个生产线程都在条件变量下等待,消费线程消费了一个数据后,一次性唤醒所有生产者线程,那么多个生产者线程就会重新竞争锁,只有1个线程竞争到并持有锁了,继续向下执行,又生产了一个数据,队列满了,然后释放了锁,此时消费者还没来得及消费,刚才阻塞在锁处的其他线程有1个竞争到了锁,就会继续往下执行,继续往满了的队列中生产数据,就会出错

同理消费者可能是多个线程,如果队列空了,多个消费线程都在条件变量下等待,生产线程生产了一个数据后,一次性唤醒所有消费者线程,那么多个消费者线程就会重新竞争锁,只有1个线程竞争到并持有锁了,继续向下执行,又消费了一个数据,队列空了,然后释放了锁,此时生产者还没来得及生产,刚才阻塞在锁处的其他线程有1个竞争到了锁,就会继续往下执行,继续从空了的队列中取走数据,就会出错

main.cc

#include "BlockQueue.hpp"
#include <pthread.h>
#include <ctime>
#include <sys/types.h>
#include <unistd.h>
using namespace std;

void *consumer(void *args)
{
    BlockQueue<int>* bq = static_cast<BlockQueue<int> *>(args);
    while(true)
    {
        //1.消费数据
        int data = 0;
        bq->Pop(&data);
        //2.对数据进行处理
        cout << "consume data: " << data << endl;
        sleep(1);
    }
    return nullptr;
}

void *productor(void *args)
{
    BlockQueue<int>* bq = static_cast<BlockQueue<int> *>(args);
    while(true)
    {
        //1.有数据
        int data = rand() % 10 + 1; //[1, 10]
        //2.进行生产
        bq->Push(data);
        cout << "produce data: " << data << endl;
    }
    return nullptr;
}

int main()
{
    srand((uint16_t)time(nullptr) ^ getpid() ^ pthread_self()); //形成更加随机的数
    BlockQueue<int> *bq = new BlockQueue<int>();
    pthread_t c, p;
    pthread_create(&c, nullptr, consumer, bq);
    pthread_create(&p, nullptr, productor, bq);
    pthread_join(c, nullptr);
    pthread_join(p, nullptr);
    return 0;
}

● 生产者要往阻塞队列里push数据,前提是得有数据,我们采用生成随机数的方式构造数据

● 消费者从阻塞队列里读走数据之后,需要处理数据,我们目前先进行简单打印方便观察现象

● 生产线程和消费线程谁先被调度,这是不确定的,取决于调度器的调度算法

● 开始时由于阻塞队列为空,不满足消费条件,因此即便消费线程先被调度,也会在条件变量下阻塞等待,直到生产线程生产了数据,这是线程同步的体现

● 当生产者一次生产出一堆数据之后,队列满了,不满足生产条件,就得等待消费者,直到消费者取走了数据,这是线程同步的体现

● 消费者每间1s消费并处理数据,而生产者没有休眠一直生产,但生产者生产速度也会跟随消费者变慢,和消费者协同起来,这是线程同步的体现

● 显示器也是公共资源,消费线程和生产线程谁竞争能力强,谁就会往显示器上打印,因此打印的顺序是不确定的,也可能会出现消息混合打印的情况

● 上述代码只是为了简单测试生产者-消费者模型,因此消费者只是简单的将生产者生产的数据打印,而我们在设计阻塞队列时,使用到了模版,因此可以设计一个任务类,生产者往阻塞队列中放任务对象,消费者从阻塞队列中取走任务并处理任务打印结果

Task.hpp

#pragma once
#include <iostream>
#include <string>
using namespace std;

const int defaultvalue = 0;

enum
{
    ok,
    div_zero,
    mode_zero,
    unknown
};

const string opers = "+-/*/%)(&";

class Task
{
public:
    Task(){}

    Task(int x, int y, char op)
        : data_x(x), data_y(y), oper(op), result(defaultvalue), code(ok)
    {}

    void Run()
    {
        switch (oper)
        {
        case '+':
            result = data_x + data_y;
            break;
        case '-':
            result = data_x - data_y;
            break;
        case '*':
            result = data_x * data_y;
            break;
        case '/':
        {
            if (data_y == 0)
                code = div_zero;
            else
                result = data_x / data_y;
        }
        break;
        case '%':
        {
            if (data_y == 0)
                code = mode_zero;
            else
                result = data_x % data_y;
        }
        break;
        default:
            code = unknown;
            break;
        }
    }

    string PrintTask()
    {
        string s;
        s = to_string(data_x);
        s += oper;
        s += to_string(data_y);
        s += "=?";
        return s;
    }

    string PrintResult()
    {
        string s;
        s = to_string(data_x);
        s += oper;
        s += to_string(data_y);
        s += "=";
        s += to_string(result);
        s += " [";
        s += to_string(code);
        s += "]";
        return s;
    }

    ~Task(){}

private:
    int data_x; //操作数
    int data_y; //操作数
    char oper; //操作符
    int result; //结果
    int code; // 结果码, 为0表示结果可信, 否则表示结果不可信
};

问题:  生产者生产数据和消费者消费的过程,本身就是互斥的啊,也就是串行的,那生产者-消费者模型的高效如何体现?

● 生产数据前,得有数据,数据从哪里来? 是取决于具体场景的,比如从网络中获取数据,这是需要花一定时间的, 而有了内存缓冲区,生产者获取数据的过程中消费者可以同时消费和处理数据

● 取走数据后,还没有结束,还需要处理数据,处理数据也是要花一定时间的,而有了内存缓冲区,消费者处理数据的过程中生产者可以同时获取并生产数据

● 而如果是多生产和多消费模型,多个生产线程可以同时从网络中获取数据,多个消费线程也可以同时处理数据

● 因此生产者-消费者模型的高效性并不体现在同步和互斥,而是在于: 处理数据的时候可以并发!!!

如何将上面的代码改为多生产多消费模型呢??

由于生产数据(push)和消费数据(pop)时,我们都加了锁,因此上述代码本身就实现了多个生产者之间的互斥,多个消费者之间的互斥,因此BlockQueue模块不需要改动,我们只需要创建多个生产者和多个消费者即可!

多生产者-多消费者模型

main.cc

#include "BlockQueue.hpp"
#include <pthread.h>
#include <ctime>
#include <sys/types.h>
#include <unistd.h>
#include "Task.hpp"

class ThreadData
{
public:
    BlockQueue<Task> *bq;
    string name;
};

void *consumer(void *args)
{
    ThreadData *td = (ThreadData *)args;
    while (true)
    {
        // 1.消费数据
        Task t;
        td->bq->Pop(&t);
        // 2.处理数据
        t(); // 仿函数
        cout << "I am " << td->name << ", consumer data: " << t.PrintResult() << endl;
    }
    return nullptr;
}

void *productor(void *args)
{
   ThreadData *td = (ThreadData *)args;
    while (true)
    {
        // 1.有数据, 从具体场景中来, 从网络中拿数据
        int data1 = rand() % 10; //[0, 9]
        int data2 = rand() % 10; //[0, 9]
        char oper = opers[rand() % opers.size()];
        Task t(data1, data2, oper); // 构建任务
        // 2.进行生产
        td->bq->Push(t);
        // for debug
        cout << "I am " << td->name << ", productor data : " << t.PrintTask() << endl;
        sleep(1);
    }
    return nullptr;
}

int main()
{
    srand((uint16_t)time(nullptr) ^ getpid() ^ pthread_self()); // 形成更加随机的数
    BlockQueue<Task> *bq = new BlockQueue<Task>();
    pthread_t c[2], p[3];

    ThreadData *td1 = new ThreadData();
    td1->bq = bq;
    td1->name = "Thread-1";
    pthread_create(&c[0], nullptr, consumer, td1);

    ThreadData *td2 = new ThreadData();
    td2->bq = bq;
    td2->name = "Thread-2";
    pthread_create(&c[0], nullptr, consumer, td2);

    ThreadData *td3 = new ThreadData();
    td3->bq = bq;
    td3->name = "Thread-3";
    pthread_create(&p[0], nullptr, productor, td3);

    ThreadData *td4 = new ThreadData();
    td4->bq = bq;
    td4->name = "Thread-4";
    pthread_create(&p[0], nullptr, productor, td4);

    ThreadData *td5 = new ThreadData();
    td5->bq = bq;
    td5->name = "Thread-4";
    pthread_create(&p[0], nullptr, productor, td5);
    
    pthread_join(c[0], nullptr);
    pthread_join(c[1], nullptr);
    pthread_join(p[0], nullptr);
    pthread_join(p[0], nullptr);
    pthread_join(p[1], nullptr);
    pthread_join(p[2], nullptr);
    return 0;
}

基于环形队列实现生产者-消费者模型

预备知识: 信号量

● 信号量本质是一把计数器,表示可用资源的数目

● 申请信号量的本质是预定资源,只要预定成功,就一定有自己的一份资源

● 申请信号量是p操作,释放信号量是v操作,p操作和v操作都是原子的

● 基于阻塞队列实现的生产者消费者模型中的公共资源我们当成一个整体来使用,因此需要加锁来解决多执行流并发访问临界资源导致数据不一致的问题

如果将公共资源划分成多部分使用,此时多线程就可以并发访问被细分的不同资源了!

信号量初始化成资源总数,有线程访问公共资源,先申请信号量,只要信号量不为0,信号量--,申请信号量就成功了,就一定能够访问到资源

信号量的接口

初始化信号量(pshared设为0,表示线程间共享, value表示信号量的初始值)

int sem_init(sem_t *sem, int pshared, unsigned int value);

 销毁信号量

int sem_destroy(sem_t *sem);

申请信号量

int sem_wait(sem_t *sem);

释放信号量

int sem_post(sem_t *sem);

 基于环形队列实现生产者-消费者模型

● 环形队列逻辑上是一个环,存储结构本质是一个线性数组,当下标越界时,取模数组长度就可以回到最开始,因此实现了环形效果

● 环形队列中的"公共资源"划分成多部分使用,不当成一个资源使用

● 开始时,消费者和生产者都指向环形队列的同一个位置

● 生产者不能把消费者套一个圈,否则生产者会覆盖掉生产过的数据,从而导致出错

● 消费者不能超过生产者,否则没有数据消费了,还消费就会出错

● 当环形队列为空为满,消费者和生产者才会指向同一个位置,访问同一份临界资源

● 当环形队列为空时,生产者先跑;为满时,消费者先跑, 这就体现了互斥和同步

● 除了为空未满,其余情况,不会指向同一个位置,那么多线程可以并发进入临界区访问资源

● 消费者认为,环形队列中的数据是资源;消费者认为,环形队列中的空间是资源!

● 开始时,没有数据,数据信号量初始化为0;开始时空间大小为N,空间信号量初始化为N

● 生产数据前,申请空间资源(空间信号量--),生产完数据后,多了一个数据(数据信号量++) 

● 消费数据前,申请数据资源(数据信号量--),消费完数据后,多了一个空间(空间信号量++)

RingQueue.hpp

#pragma once

#include <iostream>
#include <vector>
#include <semaphore.h>
using namespace std;

const int defaultsize = 5;

template <class T>
class RingQueue
{
private:
    void P(sem_t &sem)
    {
        sem_wait(&sem); //原子的
    }

    void V(sem_t &sem)
    {
        sem_post(&sem); //原子的
    }

public:
    RingQueue(int size = defaultsize)
        : _ringqueue(size), _size(size), _p_step(0), _c_step(0)
    {
        sem_init(&_space_sem, 0, _size); // 0表示线程间共享
        sem_init(&_data_sem, 0, 0); 
    }

    void Push(const T &in)
    {
        P(_space_sem);
        _ringqueue[_p_step] = in;
        _p_step++;
        _p_step %= _size;
        V(_data_sem);
    }

    void Pop(T *out)
    {
        P(_data_sem);
        *out = _ringqueue[_c_step];
        _c_step++;
        _c_step %= _size;
        V(_space_sem);
    }

    ~RingQueue()
    {
        sem_destroy(&_space_sem);
        sem_destroy(&_data_sem);
    }

private:
    vector<T> _ringqueue; //环形队列
    int _size; //环形队列大小
    int _p_step; // 生产者生产位置
    int _c_step; // 消费者消费位置
    sem_t _space_sem; // 生产者信号量
    sem_t _data_sem;  // 消费者信号量
};

main.cc

#include "RingQueue.hpp"
#include <pthread.h>
#include <unistd.h>

void *Productor(void *args)
{
    RingQueue<int> *rq = static_cast<RingQueue<int>*>(args);
    int cnt = 100;
    while(true)
    {
        rq->Push(cnt);
        cout << "produce done, data is : " << cnt << endl;
        cnt--;
    } 
}

void *Consumer(void *args)
{
    RingQueue<int> *rq = static_cast<RingQueue<int>*>(args);
    while(true)
    {
        sleep(1);
        int data = 0;
        rq->Pop(&data);
        cout << "consume done, data is : " << data << endl;
    }
}

int main()
{
    pthread_t c, p;
    RingQueue<int> *rq = new RingQueue<int>();
    pthread_create(&c, nullptr, Productor, rq);
    pthread_create(&p, nullptr, Consumer, rq);
    pthread_join(c, nullptr);
    pthread_join(p, nullptr);
    return 0;
}

环形队列中不是只可以放整形数据哦,同样可以让生产者指派任务,放到环形队列中,消费者从环形队列中取任务,然后执行任务,输出结果

main.cc

#include "RingQueue.hpp"
#include <pthread.h>
#include <unistd.h>
#include <unistd.h>
#include <ctime>
#include "Task.hpp"

void *Productor(void *args)
{
    // 1.有数据
    RingQueue<Task> *rq = static_cast<RingQueue<Task> *>(args);
    while (true)
    {
        int data1 = rand() % 10;
        int data2 = rand() % 10;
        char op = opers[rand() % opers.size()];

        // 2.生产
        Task t(data1, data2, op);
        rq->Push(t);
        cout << "productor data : " << t.PrintTask() << endl;

        sleep(1);
    }
}

void *Consumer(void *args)
{
    
    RingQueue<Task> *rq = static_cast<RingQueue<Task> *>(args);
    // 2.处理任务
    while (true)
    {
        // 1.消费数据
        Task t;
        rq->Pop(&t);
        //2.对数据做处理
        t.Run();
        cout << "consumer data: " << t.PrintResult() << endl;
    }
}

int main()
{
    srand((uint64_t)time(nullptr) ^ getpid());
    RingQueue<Task> *rq = new RingQueue<Task>();
    pthread_t c, p;
    pthread_create(&c, nullptr, Productor, rq);
    pthread_create(&p, nullptr, Consumer, rq);
    pthread_join(c, nullptr);
    pthread_join(p, nullptr);
    return 0;
}

基于环形队列的多生产者多消费者模

RingQueue.hpp 

#pragma once

#include <iostream>
#include <vector>
#include <semaphore.h>
#include "LockGuard.hpp"
using namespace std;

const int defaultsize = 5;

template <class T>
class RingQueue
{
private:
    void P(sem_t &sem)
    {
        sem_wait(&sem); // 原子的
    }

    void V(sem_t &sem)
    {
        sem_post(&sem); // 原子的
    }

public:
    RingQueue(int size = defaultsize)
        : _ringqueue(size), _size(size), _p_step(0), _c_step(0)
    {
        sem_init(&_space_sem, 0, _size); // 0表示线程间共享
        sem_init(&_data_sem, 0, 0);

        pthread_mutex_init(&_p_mutex, nullptr);
        pthread_mutex_init(&_c_mutex, nullptr);
    }

    void Push(const T &in)
    {
        P(_space_sem);
        {
            LockGuard lockguard(&_p_mutex);
            _ringqueue[_p_step] = in;
            _p_step++;
            _p_step %= _size;
        }
        V(_data_sem);
    }

    void Pop(T *out)
    {
        P(_data_sem);
        {
            LockGuard lockguard(&_c_mutex);
            *out = _ringqueue[_c_step];
            _c_step++;
            _c_step %= _size;
        }
        V(_space_sem);
    }

    ~RingQueue()
    {
        sem_destroy(&_space_sem);
        sem_destroy(&_data_sem);

        pthread_mutex_destroy(&_p_mutex);
        pthread_mutex_destroy(&_c_mutex);
    }

private:
    vector<T> _ringqueue; // 环形队列
    int _size;            // 环形队列大小

    int _p_step; // 生产者生产位置
    int _c_step; // 消费者消费位置

    sem_t _space_sem; // 生产者信号量
    sem_t _data_sem;  // 消费者信号量

    pthread_mutex_t _p_mutex; // 生产者的锁
    pthread_mutex_t _c_mutex; // 消费者的锁
};

● 基于环形队列的单生产者单消费者模型没有用到锁,只用到了信号量,因为我们把环形队列的资源划分为多部分资源使用,除了为空和为满,多线程访问的是不同位置的资源,因此不需要加锁

● 基于环形队列的多生产者多消费者模型是需要用到锁的,因为多个生产线程可能会同时往同一个位置上生产,会出现数据不一致问题,因此多个生产者需要一把锁;同理,多个消费线程可能会同时从同一个位置拿数据,也会出现数据不一致的问题,因此多个消费者也需要一把锁

● 理论上,申请锁和申请信号量的顺序是无所谓的,但建议先申请信号量,  只要资源足够,多个线程申请信号量就会成功,那么多个线程就会竞争锁,只要有1个线程竞争到了锁,其他线程就会卡在申请锁的地方,当该线程释放了锁,其他某个线程竞争到了锁就立马就可以进入临界区执行代码!而如果先申请锁,只有1个线程竞争到了锁,其余线程卡在锁的地方, 当该线程释放了锁,其他线程竞争到锁之后还要再申请信号量,这个过程是比较慢的

 main.cc

#include "RingQueue.hpp"
#include <pthread.h>
#include <unistd.h>
#include <ctime>
#include "Task.hpp"

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

class ThreadData
{
public:
    RingQueue<Task> *rq;
    string name;
};

void *Productor(void *args)
{
    // 1.有数据
    ThreadData *td = static_cast<ThreadData *>(args);
    while (true)
    {
        int data1 = rand() % 10; //[0, 9]
        int data2 = rand() % 10; //[0, 9]
        char op = opers[rand() % opers.size()];

        // 2.生产
        Task t(data1, data2, op);
        td->rq->Push(t);
        pthread_mutex_lock(&mutex);
        cout << "Thread name: " << td->name << ", productor data : " << t.PrintTask() << endl;
        pthread_mutex_unlock(&mutex);
        sleep(1);
    }
}

void *Consumer(void *args)
{
    ThreadData *td = static_cast<ThreadData *>(args);
    while (true)
    {
        // 1.消费数据
        Task t;
        td->rq->Pop(&t);
        // 2.对数据做处理
        t.Run();
        pthread_mutex_lock(&mutex);
        cout << "Thread name: " << td->name <<  ", consumer data: " << t.PrintResult() << endl;
        pthread_mutex_unlock(&mutex);
    }
}

int main()
{
    srand((uint64_t)time(nullptr) ^ getpid());
    RingQueue<Task> *rq = new RingQueue<Task>();
    pthread_t c[2], p[3];

    ThreadData *td1 = new ThreadData();
    td1->rq = rq;
    td1->name = "thread-1";
    pthread_create(&p[0], nullptr, Productor, td1);

    ThreadData *td2 = new ThreadData();
    td2->rq = rq;
    td2->name = "thread-2";
    pthread_create(&p[1], nullptr, Productor, td2);

    ThreadData *td3 = new ThreadData();
    td3->rq = rq;
    td3->name = "thread-3";
    pthread_create(&p[2], nullptr, Productor, td3);

    ThreadData *td4 = new ThreadData();
    td4->rq = rq;
    td4->name = "thread-4";
    pthread_create(&c[0], nullptr, Consumer, td4);
    
    ThreadData *td5 = new ThreadData();
    td5->rq = rq;
    td5->name = "thread-5";
    pthread_create(&c[1], nullptr, Consumer, td5);

    pthread_join(p[0], nullptr);
    pthread_join(p[1], nullptr);
    pthread_join(p[2], nullptr);
    pthread_join(c[0], nullptr);
    pthread_join(c[1], nullptr);
    return 0;
}

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

.NET6 WebAPI从基础到进阶--朝夕教育

1、环境准备 1. Visual Studio 2022 2. .NET6 平台支持 3. Internet Information Services 服务器&#xff08; IIS &#xff09; 4. Linux 服务器 【 CentOS 系统】 ( 跨平台部署使用 ) 5. Linux 服务器下的 Docker 容器&#xff08; Docker 部署使用&#xff09; …

Linux系统中进程的概念 -- 冯诺依曼体系结构,操作系统,进程概念,查看进程,进程状态,僵尸进程,孤儿进程,进程优先级,进程切换,进程调度

目录 1. 冯诺依曼体系结构 2. 操作系统(Operator System) 2.1 操作系统的概念 2.2 设计操作系统(OS)的目的 2.3 系统调用和库函数概念 3. 进程 3.1 进程的基本概念与基本操作 3.1.1 进程的基本概念 3.1.2 PCB -- 描述进程 3.1.3 task_ struct 3.1.4 查看进程 3.1.5…

4.redis通用命令

文章目录 1.使用官网文档2.redis通用命令2.1set2.2get2.3.redis全局命令2.3.1 keys 2.4 exists2.5 del(delete)2.6 expire - (失效时间)2.7 ttl - 过期时间2.7.1 redis中key的过期策略2.7.2redis定时器的实现原理 2.8 type2.9 object 3.生产环境4.常用的数据结构4.1认识数据类型…

Web项目图片视频加载缓慢/首屏加载白屏

Web项目图片视频加载缓慢/首屏加载白屏 文章目录 Web项目图片视频加载缓慢/首屏加载白屏一、原因二、 解决方案2.1、 图片和视频的优化2.1.1、压缩图片或视频2.1.2、 选择合适的图片或视频格式2.1.3、 使用图片或视频 CDN 加速2.1.4、Nginx中开启gzip 三、压缩工具推荐 一、原因…

成人教育专升本-不能盲目选择

成人教育专升本都有哪些方法?在当今时代&#xff0c;学历往往是打开职业机会的敲门砖&#xff0c;成人教育专升本成为突破职业发展瓶颈的途径&#xff0c;然而&#xff0c;你是否清楚它们之间究竟有着怎样的区别呢? 一、成人教育专升本&#xff0c;成人高考 1、考试形式 成人…

repmgr集群部署-PostgreSQL高可用保证

&#x1f4e2;&#x1f4e2;&#x1f4e2;&#x1f4e3;&#x1f4e3;&#x1f4e3; 作者&#xff1a;IT邦德 中国DBA联盟(ACDU)成员&#xff0c;10余年DBA工作经验&#xff0c; Oracle、PostgreSQL ACE CSDN博客专家及B站知名UP主&#xff0c;全网粉丝10万 擅长主流Oracle、My…

【前端】 canvas画图

一、场景描述 利用js中的canvas画图来画图&#xff0c;爱心、动画。 二、问题拆解 第一个是&#xff1a;canvas画图相关知识。 第二个是&#xff1a;动画相关内容。 三、知识背景 3.1 canvas画图相关内容 canvas画图的基本步骤 获取页面上的canvas标签对象获取绘图上下文…

深度学习——激活函数、损失函数、优化器

深度学习——激活函数、损失函数、优化器 1、激活函数1.1、一些常见的激活函数1.1.1、sigmoid1.1.2、softmax1.1.3、tanh1.1.4、ReLU1.1.5、Leaky ReLU1.1.6、PReLU1.1.7、GeLU1.1.8、ELU 1.2、激活函数的特点1.2.1、非线性1.2.2、几乎处处可微1.2.3、计算简单1.2.4、非饱和性1…

硬件设计-电源轨噪声对时钟抖动的影响

目录 定义 实际案例 总结 定义 首先了解抖动的定义&#xff0c;在ITU-T G.701中有关抖动的定义如下&#xff1a; 数字信号重要瞬间相对于其理想时间位置的短期非累积变化。 抖动是时钟或数据信号时序的短期时域变化。抖动包括信号周期、频率、相位、占空比或其他一些定时特…

搭建自己的wiki知识库(重新整理)

写在前面&#xff1a; 之前写过一篇&#xff0c;因为这次修改的内容比较多&#xff0c;所以不想在原基础上修改了&#xff0c;还是重新整理一篇吧。搭建wiki知识库&#xff0c;可以使用在线文档&#xff0c;如xx笔记、xx文档、xx博客、git仓库&#xff08;如&#xff1a;GitHu…

数据可视化的Python实现

一、GDELT介绍 GDELT ( www.gdeltproject.org ) 每时每刻监控着每个国家的几乎每个角落的 100 多种语言的新闻媒体 -- 印刷的、广播的和web 形式的&#xff0c;识别人员、位置、组织、数量、主题、数据源、情绪、报价、图片和每秒都在推动全球社会的事件&#xff0c;GDELT 为全…

【Python基础】Python知识库更新中。。。。

1、Python知识库简介 现阶段主要源于个人对 Python 编程世界的强烈兴趣探索&#xff0c;在深入钻研 Python 核心语法、丰富库函数应用以及多样化编程范式的基础上&#xff0c;逐步向外拓展延伸&#xff0c;深度挖掘其在数据分析、人工智能、网络开发等多个前沿领域的应用潜力&…

SpringCloud微服务实战系列:02从nacos-client源码分析注册中心工作原理

目录 角色与功能 工作流程&#xff1a; nacos-client关键源码分析 总结&#xff1a; 角色与功能 服务提供者&#xff1a;在启动时&#xff0c;向注册中心注册自身服务&#xff0c;并向注册中心定期发送心跳汇报存活状态。 服务消费者&#xff1a;在启动时&#xff0c;把注…

电感2222

1 电感 1电感是什么 2 电感的电流不能突变&#xff1a;电容是两端电压不能突变 3 电感只是限制电流变化速度

AI安全漏洞之VLLM反序列化漏洞分析与保姆级复现(附批量利用)

前期准备 环境需要&#xff1a;Linux&#xff08;这里使用kali&#xff09;、Anaconda 首先安装Anaconda 前言&#xff1a;最好使用linux&#xff0c;如果使用windows可能会产生各种报错&#xff08;各种各种各种&#xff01;&#xff01;&#xff01;&#xff09;&#xff…

用梗营销来启动市场

目录 为什么梗营销适合初创公司 有效的梗营销技巧 梗不仅仅是有趣的图片&#xff0c;它们是包裹在幽默中的文化时刻。对于小企业家&#xff08;以及大企业家&#xff09;&#xff0c;梗代表了一种强大且性价比高的市场推广方式。让我们分解一下为什么梗营销有效&#xff0c;以…

简单vue3前端打包部署到服务器,动态配置http请求头后端ip方法教程

vue3若依框架前端打包部署到服务器&#xff0c;需要部署到多个服务器上&#xff0c;每次打包会很麻烦&#xff0c;今天教大家一个动态配置请求头api的方法&#xff0c;部署后能动态获取(修改)对应服务器的请求ip 介绍两种方法&#xff0c;如有需要可以直接尝试步骤一&#xff…

Pyside6 --Qt设计师--简单了解各个控件的作用之:Item Views

目录 一、List View二、Tree View三、Table View四、Column View 一、List View 学习方法和Buttons一样&#xff0c;大家自己在qt设计师上面在属性编辑区进行相应的学习&#xff01; 我就先紧着qt设计师的页面进行讲解&#xff0c;部分内容查自AI。 后面有什么好用的控件或者…

【vue2+Flowable工作流,审批流前端展示组件】

文章目录 概要整体架构流程技术细节小结 概要 vue2Flowable工作流&#xff0c;审批流前端展示组件。 本人已实现activiti工作流引入vue2&#xff0c; 如有需求请移步activiti工作流单独引入vue2方法, 全网最全!!! vue全局日期格式化成年月日 时分秒 如有需求请移步vuemomen…

uni-app多环境配置动态修改

前言 这篇文章主要介绍uniapp在Hbuilderx 中&#xff0c;通过工程化&#xff0c;区分不同环境、动态修改小程序appid以及自定义条件编译&#xff0c;解决代码发布和运行时手动切换问题。 背景 当我们使用uniapp开发同一个项目发布不同的环境二级路径不同时&#xff0c;这时候…