【Linux】条件变量、基于阻塞队列的生产者消费者模型

📚 博主的专栏

🐧 Linux   |   🖥️ C++   |   📊 数据结构  | 💡C++ 算法 | 🌐 C 语言

进程是资源分配的基本单位,线程是调度的基本单位,线程是在进程内部运行的(是进程内部的控制序列,本质是在进程地址空间内运行),如何理解线程是在进程内部运行的?因为在linux中,线程是用进程pcb来模拟的,所有的PCB共享虚拟地址空间,共享页表。

上篇文章:线程id、互斥

下篇文章:POSIX信号量、基于环形队列的生产消费模型、线程池

目录

条件变量

同步概念与竞态条件

a.核心接口函数

1. 初始化条件变量

2. 销毁条件变量

3. 等待条件变量

4. 限时等待

5. 唤醒单个线程

6. 唤醒所有线程

 b.认识条件变量(举例)

场景设定

为什么 pthread_cond_wait 需要互斥量?

错误的设计 

条件变量使用规范

c.生产消费模型

基于BlockingQueue的生产者消费者模型

完整代码:

条件变量

当一个线程互斥地访问某个变量时,它可能发现在其它线程改变状态之前,它什么也做不了。

例如一个线程访问队列时,发现队列为空,它只能等待,只到其它线程将一个节点添加到队列中。这种情况就需要用到条件变量。

同步概念与竞态条件

同步:在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问题,叫做同步
竞态条件:因为时序问题,而导致程序异常,我们称之为竞态条件。在线程场景下,这种问题也不难理解

a.核心接口函数

类似于线程锁的接口

1. 初始化条件变量
int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr);
  • 功能:动态初始化条件变量。

  • 参数

    • cond:指向条件变量的指针。

    • attr:属性参数,通常设为NULL(使用默认属性)。

  • 静态初始化(无需调用init):

    pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

2. 销毁条件变量
int pthread_cond_destroy(pthread_cond_t *cond);
  • 功能:释放条件变量占用的资源。

  • 注意事项:确保没有线程在等待该条件变量后再调用此函数。

3. 等待条件变量
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
  • 功能:阻塞当前线程,直到其他线程通过signalbroadcast唤醒它。

  • 关键行为

    • 原子操作:调用时会自动释放关联的互斥锁,并进入等待状态。

    • 被唤醒后:函数返回前会重新获取互斥锁

  • 使用模式(必须与互斥锁配合):

    pthread_mutex_lock(&mutex);
    while (条件不满足) {  // 必须用循环检查条件,防止虚假唤醒pthread_cond_wait(&cond, &mutex);
    }
    // 执行临界区操作
    pthread_mutex_unlock(&mutex);
4. 限时等待
int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex, const struct timespec *abstime);
  • 功能:与pthread_cond_wait类似,但可设置超时时间。

  • 参数abstime是绝对时间(例如clock_gettime(CLOCK_REALTIME, &ts)获取当前时间后加上超时偏移)。

5. 唤醒单个线程
int pthread_cond_signal(pthread_cond_t *cond);
  • 功能:唤醒至少一个正在等待该条件变量的线程(具体唤醒哪个线程取决于调度策略)。

6. 唤醒所有线程
int pthread_cond_broadcast(pthread_cond_t *cond);
  • 功能:唤醒所有正在等待该条件变量的线程。

 b.认识条件变量(举例)

主线程唤醒新线程,或全部唤醒

场景设定

假设有一家网红冰淇淋店,每天限量供应10个冰淇淋。顾客(线程)络绎不绝,但规则是:

  1. 每次只能有一个顾客购买(互斥锁保护)。

  2. 如果冰淇淋卖完了,顾客必须排队等待,直到店员补货(条件变量通知)。

  3. 补货后,所有等待的顾客可以重新尝试购买。

#include <pthread.h>// 全局变量
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;  // 互斥锁:控制购买权
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;     // 条件变量:通知补货
int ice_cream = 0;                                  // 当前冰淇淋数量(初始为0,需补货)// 店员线程(补货)
void* staff(void* arg) {while (1) {pthread_mutex_lock(&mutex);if (ice_cream == 0) {                       // 如果冰淇淋卖光了ice_cream = 10;                         // 补货10个printf("店员:新到货10个冰淇淋!\n");pthread_cond_broadcast(&cond);           // 大喊一声通知所有顾客}pthread_mutex_unlock(&mutex);sleep(5);                                    // 每隔5秒检查一次是否需要补货}
}// 顾客线程(购买)
void* customer(void* arg) {int id = *(int*)arg;pthread_mutex_lock(&mutex);while (ice_cream == 0) {                        // 如果没货了printf("顾客%d:\"淦,卖完了?我等!\"\n", id);pthread_cond_wait(&cond, &mutex);            // 坐下等待店员喊补货}ice_cream--;                                     // 成功购买printf("顾客%d:抢到一个冰淇淋!剩余:%d\n", id, ice_cream);pthread_mutex_unlock(&mutex);return NULL;
}int main() {pthread_t staff_thread;pthread_t customer_threads[15];int ids[15];// 启动店员线程(持续补货)pthread_create(&staff_thread, NULL, staff, NULL);// 模拟15个顾客疯狂抢购for (int i=0; i<15; i++) {ids[i] = i;pthread_create(&customer_threads[i], NULL, customer, &ids[i]);usleep(200000);  // 让顾客们陆续到达}// 等待所有线程结束for (int i=0; i<15; i++) pthread_join(customer_threads[i], NULL);return 0;
}

互斥锁(mutex)

  • 相当于“购买权”,一次只允许一个顾客查看/购买冰淇淋。

  • 顾客进店前必须抢到锁(pthread_mutex_lock)。

条件变量(cond)

  • 当冰淇淋卖光时(ice_cream == 0),顾客调用pthread_cond_wait进入等待队列,同时释放锁(让其他顾客可以继续尝试)。pthread_cond_wait在被调用的时候,除了让自己继续排队等待,还会自己释放传入的锁。

  • 店员补货后,通过pthread_cond_broadcast喊一嗓子:“补货啦!”,唤醒所有等待的顾客。

循环检查条件(while而非if

  • 假设10个顾客被唤醒,但只有前10个能买到,第11个发现ice_cream又变0了,继续等待。

  • 防止虚假唤醒(比如被意外吵醒但没补货)。

顾客0:抢到一个冰淇淋!剩余:9  
顾客1:抢到一个冰淇淋!剩余:8  
...(前10个顾客购买成功)  
顾客10:\"淦,卖完了?我等!\"  
店员:新到货10个冰淇淋!  
顾客10:抢到一个冰淇淋!剩余:9  
顾客11:抢到一个冰淇淋!剩余:8  
...

为什么 pthread_cond_wait 需要互斥量?


条件等待是线程间同步的一种手段,如果只有一个线程,条件不满足,一直等下去都不会满足,所以必须要有一个线程通过某些操作,改变共享变量,使原先不满足的条件变得满足,并且友好的通知等待在条件变量上的线程。
条件不会无缘无故的突然变得满足了,必然会牵扯到共享数据的变化。所以一定要用互斥锁来保护。没有互斥锁就无法安全的获取和修改共享数据
 

错误的设计 

// 错误的设计
pthread_mutex_lock(&mutex);
while (condition_is_false) {
pthread_mutex_unlock(&mutex);
//解锁之后,等待之前,条件可能已经满足,信号已经发出,但是该信号可能被错过
pthread_cond_wait(&cond);
pthread_mutex_lock(&mutex);
} p
thread_mutex_unlock(&mutex);

由于解锁和等待不是原子操作。调用解锁之后, pthread_cond_wait 之前,如果已经有其他线程获取到互斥量,摒弃条件满足,发送了信号,那么 pthread_cond_wait 将错过这个信号,可能会导致线程永远阻塞在这个 pthread_cond_wait 。所以解锁和等待必须是一个原子操作。
int pthread_cond_wait(pthread_cond_ t *cond,pthread_mutex_ t * mutex); 进入该函数后,会去看条件量等于0?等于,就把互斥量变成1,直到cond_ wait返回,把条件量改成1,把互斥量恢复成原样。后面继续以生产者消费者模型来深入讲解。
请特别注意代码中的注释

条件变量使用规范

等待条件代码

pthread_mutex_lock(&mutex);
while (条件为假)
pthread_cond_wait(cond, mutex);

修改条件
 

pthread_mutex_unlock(&mutex);
给条件发送信号代码
pthread_mutex_lock(&mutex);

设置条件为真
 

pthread_cond_signal(cond);
pthread_mutex_unlock(&mutex)

c.生产消费模型

生产者消费者就是一个多执行流并发的模型

为何要使用生产者消费者模型

生产者消费者模式就是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。这个阻塞队列就是用来给生产者和消费者解耦的。

生产者消费者模型优点

(供应商、超市(一段内存)方便面(数据)、消费者的例子)

  • 解耦
  • 支持并发
  • 支持忙闲不均

思考切入点:“321”原则

例如面试时问到生产者消费者模型:就按照321介绍,再说生产者消费者模型优缺点。

        1.一个交易场所(特定数据结构形式存在的一段内存空间)

        2.两种角色(生产角色,消费角色)生产线程,和消费线程

        3.三种关系(生产和生产、消费和消费、生产和消费)

例如供应商(生产者)都是竞争关系,消费者之间是互斥关系(想一想如果商品出现供不应求),生产者和消费者(供应商正在商品价上放商品还未标价)之间是互斥关系,并且还要维护一定程度的(如果出现供大于求,提醒消费者来买,或者供不应求,消费者提醒供货商出货)同步关系。

如果我要实现生产消费模型,本质就是通过代码,实现321原则,用锁和条件变量(或其他方式)来实现三种关系。

基于BlockingQueue的生产者消费者模型

BlockingQueue

在多线程编程中阻塞队列(Blocking Queue)是一种常用于实现生产者和消费者模型的数据结构。其与普通的队列区别在于,当队列为空时,从队列获取元素的操作将会被阻塞,直到队列中被放入了元素;当队列满时,往队列里存放元素的操作也会被阻塞,直到有元素被从队列中取出(以上的操作都是基于不同的线程来说的,线程在对阻塞队列进程操作时会被阻塞)

C++ queue模拟阻塞队列的生产消费模型

实际上管道(管道自带同步和互斥关系)也是一种生产消费模型。

首先我们看这个代码:当在使用条件变量线程在等待的时候,会将锁释放,但是当wait结束,函数返回,没有锁还是在临界区。实际上,在函数返回时,必须先参与锁的竞争,重新申请到锁,该函数才会返回。因此醒来的时候也是有锁的。

 void Equeue(const T& in){pthread_mutex_lock(&_mutex);if(IsFull())//队列是否满了{//生产这不能生产,必须等待//在临界区里面在等待,但锁没有释放,即便等待了,但是锁没有释放,注定消费者pop的时候也无法拿到锁,死锁pthread_cond_wait(&_p_cond, &_mutex);//函数返回的时候,不还在临界区吗,并且锁不是已经被释放了吗//pthread_cond_wait在被调用的时候,除了让自己继续排队等待,还会自己释放传入的锁}pthread_mutex_unlock(&_mutex);}

生产者生产后,一定知道队列不为空,消费者消费一个后一定知道队列没有满,因此生产者和消费者是互相唤醒的,互相通知。

 //将数据带出去的接口:void Pop(T* out){pthread_mutex_lock(&_mutex);if(IsEmpty()){pthread_cond_wait(&_c_cond, &_mutex);//等待唤醒}//1.没有空 || 2.被唤醒了//说明一定有数据,因此要消费*out = _block_queue.front();_block_queue.pop();//消费者消费一个数据,就一定知道队列不是满的//生产者最清楚队列一定不为空pthread_mutex_unlock(&_mutex);pthread_cond_signal(&_p_cond);//生产者没睡着,此操作无效}//入队列void Equeue(const T& in){pthread_mutex_lock(&_mutex);if(IsFull())//队列是否满了{//生产这不能生产,必须等待//在临界区里面在等待,但锁没有释放,即便等待了,但是锁没有释放,注定消费者pop的时候也无法拿到锁,死锁pthread_cond_wait(&_p_cond, &_mutex);//函数返回的时候,不还在临界区吗,并且锁不是已经被释放了吗//等待唤醒//pthread_cond_wait在被调用的时候,除了让自己继续排队等待,还会自己释放传入的锁}// 1.没有满 || 2.被唤醒了//因此就可以生产到阻塞队列(还没解锁,数据不会被拿走,一定是有数据的)_block_queue.push(in);pthread_mutex_unlock(&_mutex);//有数据了,就要让消费者消费pthread_cond_signal(&_c_cond);//消费者没睡着,此操作无效,}

为什么唤醒操作放在解锁后

都是可以的,一旦把任意一方唤醒,他们都仍然需要竞争锁,因为在等待的时候他们都将锁释放了,如果竞争锁失败了,就会继续等待,等另外线程释放锁,再返回。局部互斥整体同步。

可以设置一个策略,到达设定的容量的时候就通知。

int low_water                int high_water

条件尚未满足,但是线程被异常唤醒的请款,叫做伪唤醒,没有竞争成功的线程会再次判断队列是否为空,为空就再次等待,不为空再继续,再进行获取资源,因此我们用到while,可以保证代码的鲁棒性(健壮性)

如果,生产者只有一个,消费者有两个,当两个消费者都在等待的时候,生产者采用

pthread_cond_broadcast()唤醒线程怎么办(此时只有一个能提供给消费者消费的资源)

两个消费者都被唤醒了,其中一个竞争锁失败的线程在等待(不在条件变量下等),另一个线程在锁那里等,等拿了资源的线程释放锁,等拿了资源的线程释放锁之后,就会持有到锁,就会继续往后走,但是队列已经没有资源了。

完整代码:

阻塞队列封装1.0

#pragma once#include <iostream>
#include <string>
#include <queue>
#include <pthread.h>const static int defaultcap = 5;
template <class T>
class BlockQueue
{
private:bool IsFull(){return _block_queue.size() == _max_cap;}bool IsEmpty(){return _block_queue.empty();}public:BlockQueue(int cap = defaultcap): _max_cap(cap){pthread_mutex_init(&_mutex, nullptr);pthread_cond_init(&_p_cond, nullptr);pthread_cond_init(&_c_cond, nullptr);}// 将数据带出去的接口:void Pop(T *out){pthread_mutex_lock(&_mutex);if (IsEmpty()){pthread_cond_wait(&_c_cond, &_mutex); // 等待唤醒}// 1.没有空 || 2.被唤醒了// 说明一定有数据,因此要消费*out = _block_queue.front();_block_queue.pop(); // 消费者消费一个数据,就一定知道队列不是满的// 生产者最清楚队列一定不为空pthread_mutex_unlock(&_mutex);pthread_cond_signal(&_p_cond); // 生产者没睡着,此操作无效}// 入队列void Equeue(const T &in){pthread_mutex_lock(&_mutex);if (IsFull()) // 队列是否满了{// 生产这不能生产,必须等待// 在临界区里面在等待,但锁没有释放,即便等待了,但是锁没有释放,注定消费者pop的时候也无法拿到锁,死锁pthread_cond_wait(&_p_cond, &_mutex); // 函数返回的时候,不还在临界区吗,并且锁不是已经被释放了吗// 等待唤醒// pthread_cond_wait在被调用的时候,除了让自己继续排队等待,还会自己释放传入的锁}// 1.没有满 || 2.被唤醒了//因此就可以生产到阻塞队列(还没解锁,数据不会被拿走,一定是有数据的)_block_queue.push(in);pthread_mutex_unlock(&_mutex);// 有数据了,就要让消费者消费pthread_cond_signal(&_c_cond); // 消费者没睡着,此操作无效,// 如果,生产者只有一个,消费者有两个,当两个消费者都在等待的时候,生产者采用// pthread_cond_broadcast()唤醒线程怎么办(此时只有一个能提供给消费者消费的资源}~BlockQueue(){pthread_mutex_destroy(&_mutex);pthread_cond_destroy(&_p_cond);pthread_cond_destroy(&_c_cond);}private:std::queue<T> _block_queue; // 临界资源int _max_cap;               // 队列最大值pthread_mutex_t _mutex;     // 定义一把锁pthread_cond_t _p_cond;     // 给生产者提供的条件变量pthread_cond_t _c_cond;     // 给消费者提供的条件变量
};

生产者消费者1.0:

#include"BlockQueue.hpp"
#include<pthread.h>
//做随机值
#include<ctime>
#include<unistd.h>
//消费者
void *Consumer(void *args)
{BlockQueue<int>* bq = static_cast<BlockQueue<int> *>(args);//生产者、消费者看到同一个队列while(true){sleep(2);int data = 0;//1.获取数据:从阻塞队列中把任务拿走bq->Pop(&data);//2.处理数据std::cout<<"Consumer ->" << data << std::endl;}
}
//生产者
void* Productor(void* args)
{srand(time(nullptr) ^ getpid());BlockQueue<int>* bq = static_cast<BlockQueue<int> *>(args);//生产者、消费者看到同一个队列while(true){//1.构建数据int data = rand()% 10 + 1;//[1,10]//2.生产数据bq->Equeue(data);std::cout << "Producer -->" << data << std::endl; }
}int main()
{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;
}

此时的代码是,生产者一直生产,消费者2s消费一次:因为产品饱和,后续就需要消费者消费一次再生产一次。因为是阻塞队列

当我们让生产者2s生产一个,消费者一直消费:就会出现生产者生产一个消费者消费一个的现象:

 阻塞队列除了传整数还能传

既然像阻塞队列当中投递数据,就也能投递任务:

既然是传模版,那么就能传自定义类,我们将结构化数据封装成任务,再传给阻塞队列。

封装一个任务类:

class Task
{
public:Task(){}Task(int x, int y) : _x(x), _y(y){}// 做加法void Excute(){_result = _x + _y;}void operator()(){Excute();}std::string debug(){std::string msg = std::to_string(_x) + "+" + std::to_string(_y) + "=?";return msg;}std::string result(){std::string msg = std::to_string(_x) + "+" + std::to_string(_y) + "=" + std::to_string(_result);return msg;}private:int _x;int _y;int _result;
};

阻塞队列封装2.0,就是修改传的模版参数以及获取数据

#pragma once#include <iostream>
#include <string>
#include <queue>
#include <pthread.h>
#include "Task.hpp"
const static int defaultcap = 5;
template <class T>
class BlockQueue
{
private:bool IsFull(){return _block_queue.size() == _max_cap;}bool IsEmpty(){return _block_queue.empty();}public:BlockQueue(int cap = defaultcap): _max_cap(cap){pthread_mutex_init(&_mutex, nullptr);pthread_cond_init(&_p_cond, nullptr);pthread_cond_init(&_c_cond, nullptr);}// 将数据带出去的接口:void Pop(T *out){pthread_mutex_lock(&_mutex);if (IsEmpty()){pthread_cond_wait(&_c_cond, &_mutex); // 等待唤醒}// 1.没有空 || 2.被唤醒了// 说明一定有数据,因此要消费*out = _block_queue.front();_block_queue.pop(); // 消费者消费一个数据,就一定知道队列不是满的// 生产者最清楚队列一定不为空pthread_mutex_unlock(&_mutex);pthread_cond_signal(&_p_cond); // 生产者没睡着,此操作无效}// 入队列void Equeue(const T &in){pthread_mutex_lock(&_mutex);if (IsFull()) // 队列是否满了{// 生产这不能生产,必须等待// 在临界区里面在等待,但锁没有释放,即便等待了,但是锁没有释放,注定消费者pop的时候也无法拿到锁,死锁pthread_cond_wait(&_p_cond, &_mutex); // 函数返回的时候,不还在临界区吗,并且锁不是已经被释放了吗// 等待唤醒// pthread_cond_wait在被调用的时候,除了让自己继续排队等待,还会自己释放传入的锁}// 1.没有满 || 2.被唤醒了//因此就可以生产到阻塞队列(还没解锁,数据不会被拿走,一定是有数据的)_block_queue.push(in);pthread_mutex_unlock(&_mutex);// 有数据了,就要让消费者消费pthread_cond_signal(&_c_cond); // 消费者没睡着,此操作无效,// 如果,生产者只有一个,消费者有两个,当两个消费者都在等待的时候,生产者采用// pthread_cond_broadcast()唤醒线程怎么办(此时只有一个能提供给消费者消费的资源}~BlockQueue(){pthread_mutex_destroy(&_mutex);pthread_cond_destroy(&_p_cond);pthread_cond_destroy(&_c_cond);}private:std::queue<T> _block_queue; // 临界资源int _max_cap;               // 队列最大值pthread_mutex_t _mutex;     // 定义一把锁pthread_cond_t _p_cond;     // 给生产者提供的条件变量pthread_cond_t _c_cond;     // 给消费者提供的条件变量
};

 生产消费:

#include"BlockQueue.hpp"
#include<pthread.h>
//做随机值
#include<ctime>
#include<unistd.h>
//消费者
void *Consumer(void *args)
{BlockQueue<Task>* bq = static_cast<BlockQueue<Task> *>(args);//生产者、消费者看到同一个队列while(true){// sleep(2);// int data = 0;//1.获取数据:从阻塞队列中把任务拿走Task t;bq->Pop(&t);//2.处理数据t.Excute();std::cout<<"Consumer ->" << t.result() << std::endl;}
}
//生产者
void* Productor(void* args)
{srand(time(nullptr) ^ getpid());BlockQueue<Task>* bq = static_cast<BlockQueue<Task> *>(args);//生产者、消费者看到同一个队列while(true){sleep(2);//1.构建数据int x = rand()% 10 + 1;//[1,10]usleep(x*1000);int y = rand()% 10 + 1;Task t(x, y);//2.生产数据// bq->Equeue(x, y);//临时变量无法被引用bq->Equeue(t);std::cout << "Producer -->" << t.debug() << std::endl; }
}int main()
{BlockQueue<Task> *bq = new BlockQueue<Task>();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;
}

运行结果: 

通过前面所学的functional包装器来做任务传函数对象 

// 等同于typedef std::function<void()> task_t;
using task_t = std::function<void()>;//返回值是void参数为空的函数对象,类似于一个类,因为底层是仿函数

在未来我们就能这样写;

void Download()
{std::cout << "我是一个下载的任务" << std::endl;
}

生产者消费者3.0 

#include"BlockQueue.hpp"
#include<pthread.h>
//做随机值
#include<ctime>
#include<unistd.h>
#include "Task.hpp"
//消费者
void *Consumer(void *args)
{BlockQueue<task_t>* bq = static_cast<BlockQueue<task_t> *>(args);//生产者、消费者看到同一个队列while(true){// sleep(2);// int data = 0;//1.获取数据:从阻塞队列中把任务拿走// Task t;task_t t;bq->Pop(&t);//2.处理数据// t.Excute();t();// std::cout<<"Consumer ->" << t.result() << std::endl;}
}
//生产者
void* Productor(void* args)
{srand(time(nullptr) ^ getpid());BlockQueue<task_t>* bq = static_cast<BlockQueue<task_t> *>(args);//生产者、消费者看到同一个队列while(true){sleep(2);//1.构建数据// int x = rand()% 10 + 1;//[1,10]// usleep(x*1000);// int y = rand()% 10 + 1;// Task t(x, y);//2.生产数据// bq->Equeue(x, y);//临时变量无法被引用bq->Equeue(Download);std::cout << "Producer --> Download" << std::endl; }
}int main()
{BlockQueue<task_t> *bq = new BlockQueue<task_t>();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;
}

多生产,多消费:

在生产消费模型当中,产生任务也要花时间,消费者处理任务需要花时间,使用多生产多消费的原因是因为,获取任务和处理任务就会出现并发,生产者放任务到某个地方的时候和其他生产者产生任务是并发的,因此我们所说的高效,说的是,这些并发。

生产和消费不能只看,他们从阻塞队列放数据和拿数据的时间,真实的场景下,放数据和拿数据的时间只占很小的比重,处理数据、获取数据才占更大的比重,因此多线程在根本上是为了解决:让处理数据和获取数据有更好的并发度

为什么加条件变量等待pthread_cond_wait,一定是加在加锁解锁之间临界区里的呢?而且还添加了要传mutex。

因为无论是生产者还是消费者,要进行消费,访问公共资源,要知道条件是否满足,需要先检测资源的状态,这种检测,这种查看是否满足条件,想要查看的结果也是属于临界区的,就是属于访问临界区,因此注定要在临界区中设置等待。

复习system V信号量:【Linux】System V信号量与IPC资源管理简易讲解-CSDN博客

申请信号量的本质就是对公共资源的一种预定机制

以前在讲进程间通信中说过,信号量是一个描述资源数目的计数器,要访问资源时,需要先申请信号量。信号量和互斥锁之间的差别在于:不用像互斥锁一样在这里先做判断了,因为信号量本身就是一个计数器,描述临界区中资源数目得多少,所以在进入临界区的时候,通过申请信号量的方式来的值条件是否满足,不满足就阻塞。

下一篇博客将讲解:POSIX信号量

结语:

       随着这篇博客接近尾声,我衷心希望我所分享的内容能为你带来一些启发和帮助。学习和理解的过程往往充满挑战,但正是这些挑战让我们不断成长和进步。我在准备这篇文章时,也深刻体会到了学习与分享的乐趣。    

         在此,我要特别感谢每一位阅读到这里的你。是你的关注和支持,给予了我持续写作和分享的动力。我深知,无论我在某个领域有多少见解,都离不开大家的鼓励与指正。因此,如果你在阅读过程中有任何疑问、建议或是发现了文章中的不足之处,都欢迎你慷慨赐教     。

        你的每一条反馈都是我前进路上的宝贵财富。同时,我也非常期待能够得到你的点赞、收藏,关注这将是对我莫大的支持和鼓励。当然,我更期待的是能够持续为你带来有价值的内容。

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

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

相关文章

32-工艺品商城小程序

技术&#xff1a; 基于 B/S 架构 SpringBootMySQLvueelementuiuniapp 环境&#xff1a; Idea mysql maven jdk1.8 node 可修改为其他类型商城 用户端功能 1.系统首页展示轮播图及工艺品列表 2.分类模块:展示产品的分类类型 3.购物车:进行商品多选结算 或者批量管理操作 4.…

SLAM | 激光SLAM中的退化问题

在激光SLAM中,判断退化环境的核心是通过数学建模分析环境特征对位姿估计的约束能力。除了LOAM中提出的退化因子D外,还存在多种基于表达式和阈值设定的方法。以下是几种典型方法及其实现原理: 1. 协方差矩阵特征值分析 原理:通过分析点云协方差矩阵的特征值分布,判断环境中…

【2025最新版】火鸟门户v8.5系统源码+PC、H5、小程序 +数据化大屏插件

一.介绍 火鸟地方门户系统V8.5源码 系统包含4端&#xff1a; PCH5小程序APP 二.搭建环境 系统环境&#xff1a;CentOS、 运行环境&#xff1a;宝塔 Linux 网站环境&#xff1a;Nginx 1.2.22 MySQL 5.6 PHP-7.4 常见插件&#xff1a;fileinfo &#xff1b; redis 三.测…

PHP腾讯云人脸核身获取NONCE ticket

参考腾讯云官方文档&#xff1a; 人脸核身 获取 NONCE ticket_腾讯云 前提条件&#xff0c;已经成功获取了access token。 获取参考文档&#xff1a; PHP腾讯云人脸核身获取Access Token-CSDN博客 public function getTxFaceNonceTicket($uid) {$access_token file_get_c…

多人3D游戏完整实现方案

以下是一份完整的代码实现方案,涵盖架构设计、核心模块实现和部署流程。我们以 多人3D游戏 为例,结合之前讨论的Nano服务端框架和Unity客户端: 技术栈 模块技术选型服务端Golang + Nano框架 + MongoDB客户端Unity 2022 + C# + Mirror Networking通信协议Protobuf + WebSock…

【Linux我做主】GDB调试工具完全指南

Linux下GDB调试工具完全指南&#xff1a;25个核心命令详解与实战示例 github地址 有梦想的电信狗 前言 GDB&#xff08;GNU Debugger&#xff09;是Linux开发中不可或缺的调试工具&#xff0c;尤其在定位代码逻辑错误和内存问题时表现卓越。本文基于实际开发经验&#xff0…

QT中栅格模式探索

1、Qt中选择了栅格模式&#xff0c;如下图所示&#xff1a; 2、在进行整个大的UI界面布局时&#xff0c;需了解每个控件所需要选择的属性sizePolicy。 sizePolicy包含如下几种选择&#xff1a; 3、举个例子&#xff1a;此时整个UI界面&#xff0c;我采用了栅格模式&#xf…

【计算机网络】3数据链路层①

这篇笔记专门讲数据链路层的功能。 2.功能 数据链路层的主要任务是让帧在一段链路上或一个网络中传输。 2.1.封装成帧(组帧) 解决的问题:①帧定界②帧同步③透明传输 实现组帧的方法通常有以下种。 2.1.1.字符计数法 原理:在每个帧开头,用一个定长计数字段来记录该…

[区块链lab2] 构建具备加密功能的Web服务端

实验目标&#xff1a; 掌握区块链中密码技术的工作原理。在基于Flask框架的服务端中实现哈希算法的加密功能。 实验内容&#xff1a; 构建Flash Web服务器&#xff0c;实现哈希算法、非对称加密算法的加密功能。 实验步骤&#xff1a; 哈希算法的应用&#xff1a;创建hash…

蓝桥杯之前缀和

一维前缀 解题思路 看到“区间之和”问题&#xff0c;直接想到“前缀和” 前缀和的核心公式&#xff1a; sum[i]sum[i−1]a[i] 利用前缀和求区间和 [l,r] 的公式&#xff1a; 区间和sum[r]−sum[l−1] 解题步骤模板 输入数组&#xff1a; 读取数组长度 n 和查询次数 m。 读…

【学习笔记】计算机网络(八)—— 音频/视频服务

第8章 互联网上的音频/视频服务 文章目录 第8章 互联网上的音频/视频服务8.1概述8.2 流式存储音频/视频8.2.1 具有元文件的万维网服务器8.2.2 媒体服务器8.2.3 实时流式协议 RTSP 8.3 交互式音频/视频8.3.1 IP 电话概述8.3.2 IP电话所需要的几种应用协议8.3.3 实时运输协议 RTP…

【WRF运行】解决metgrid生成文件太大无内存!

目录 方法:改变工作目录运行 metgrid.exe参考由于我的运行内存过小,当研究区较大时,metgrid生成文件内存太大,导致每次运行都报错,此时可更改工作目录(空余文件夹)以运行 metgrid.exe(并非必须在wrf安装目录下运行!!!)。 metgrid.exe 本身不支持直接通过参数或 nam…

基于 Django 进行 Python 开发

基于 Django 进行 Python 开发涉及多个方面的知识点,以下为你详细介绍: 1. Django 基础 项目与应用创建 借助django-admin startproject project_name来创建新的 Django 项目。利用python manage.py startapp app_name创建新的应用。项目结构 理解项目各文件和目录的作用,像…

【sylar-webserver】8 HOOK模块

文章目录 知识点HOOK实现方式非侵入式hook侵入式hook ⭐⭐⭐ 覆盖系统调用接口获取被全局符号介入机制覆盖的系统调用接口 具体实现C 模板成员函数继承 和 成员函数指针类型匹配 ⭐⭐⭐⭐⭐FdCtx 和 FdManager ⭐⭐判断socket的小技巧FdCtxFdManager connect hook ⭐do_io模板 …

SpringAI+DeepSeek大模型应用开发——1 AI概述

AI领域常用词汇 LLM&#xff08;LargeLanguage Model&#xff0c;大语言模型&#xff09; 能理解和生成自然语言的巨型AI模型&#xff0c;通过海量文本训练。例子&#xff1a;GPT-4、Claude、DeepSeek、文心一言、通义干问。 G&#xff08;Generative&#xff09;生成式: 根据上…

SpringBoot 基本原理

SpringBoot 为我们做的自动配置&#xff0c;确实方便快捷&#xff0c;但一直搞不明白它的内部启动原理&#xff0c;这次就来一步步解开 SpringBoot 的神秘面纱&#xff0c;让它不再神秘。 目录 SpringBootApplication 背后的秘密 Configuration ComponentScan EnableAutoC…

2025.4.17总结

工作&#xff1a;今天对需求的测试设计进行了完善&#xff0c;然后&#xff0c;对测试设计进行了评审&#xff0c;最后提了个问题单。 反思这个过程&#xff0c;要说不足的地方&#xff0c;就是评审的时候总觉得自己吐字不清晰&#xff0c;表达能力早就想提升了&#xff0c;但…

2021-11-14 C++三七二十一数

缘由c编程怎么写&#xff0c;紧急求解-编程语言-CSDN问答 void 三七二十一数() {//缘由https://ask.csdn.net/questions/7566632?spm1005.2025.3001.5141int n 0, a 0, b 0, p 1;std::cin >> n;while (n--){std::cin >> a >> b;while (a<b){if (a %…

大模型面经 | DeepSpeed中ZeRO-1、ZeRO-2和ZeRO-3的区别是什么?

大家好,我是皮先生!! 今天给大家分享一些关于大模型面试常见的面试题,希望对大家的面试有所帮助。 往期回顾: 大模型面经 | 春招、秋招算法面试常考八股文附答案(RAG专题一) 大模型面经 | 春招、秋招算法面试常考八股文附答案(RAG专题二) 大模型面经 | 春招、秋招算法…

spring boot 文件上传

1.编写文件上传的表单页面 <!DOCTYPE html> <html lang"en" xmlns:th"http://www.thymeleaf.org"> <head><meta charset"UTF-8"><meta http-equiv"Content-Type" content"text/html; charsetUTF-8&qu…