当前位置: 首页 > news >正文

Linux 生产消费者模型

 💓博主CSDN主页:麻辣韭菜💓

⏩专栏分类:Linux初窥门径

🚚代码仓库:Linux代码练习🚚

🌹关注我🫵带你学习更多Linux知识
  🔝 

前言

 1. 生产消费者模型

1.1  什么是生产消费者模型?

1.2 生产消费者模型原则

1.3 生产消费者模型的优点 

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

2.1 单生产单消费模型  

2.2 多生产多消费 

3. POSIX 信号量

POSIX 信号量有两种类型:

POSIX 信号量的基本操作:

4. 基于循环队列实现生产消费者模型

4.1 多生产多消费

环形队列的优缺点:

阻塞队列的优缺点:


前言

生产者-消费者模型是一个经典的并发编程问题,它描述了两种角色:生产者和消费者。生产者负责生成数据,而消费者则负责消费这些数据。这个模型通常用于处理多线程或多进程环境中的资源分配问题。 

 1. 生产消费者模型

1.1  什么是生产消费者模型?

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

上面的名词有些抽象,我们直接用生活中案例来举例子,大家就会豁然开朗。

 超市工作模式:

超市需要从工厂拿货,工厂则需要提供给超市商品

消费者在超市消费,超市需要向顾客提供商品 

 超市的作用就是平衡消费者和工厂供需平衡

为什么这么说?

简单来说就是要做到 顾客可以在超市买到想要购买的商品,工厂也能同超市完成足量的需求订单,超市这样就可以为双方提供便利。

顾客再也不用到工厂去买商品

工厂也不需要将商品亲自送到顾客手中。

如果没有超市,顾客直接去工厂消费,工厂生产出来商品再送到顾客手中,这种关系就是高度相互依赖,离开谁都不能干。这就是传说中的强耦合关系。

超市的出现,极大了提高效率,从而顾客工厂之间不再单方面的依赖。使得它们之间依赖度降低。而这就是传说的中解耦。 

生产者消费者模型的本质:忙闲不均 

我们再回到编程的视角

  •  工厂 —> 生产者
  •  顾客 —> 消费者
  •  超市 —> 某种容器  

这样我们就可以利用线程来干事了,线程充当生产者和消费者。利用STL的队列容器(缓冲区)充当超市。 常见的有 阻塞队列 和 环形队列

在实现中,超市不可能只面向一个顾客,一个工厂。在多线程中,也就意味着它们都能看到这个队列(超市),那么必须就要让线程之间存在互斥与同步。对于互斥与同步不理解的可以看 Linux 线程的同步与互斥

从上面我们就可以的得出它们之间关系。

生产者VS生产者:互斥

 一张图解释一切,这么多汽车生产商,相互竞争,对于多线程之间也是一样,所以需要互斥。

消费者VS消费者:互斥 

比如宝马4S店里,只剩最后一辆宝马7系,如果这时来了两个消费者,张三李四都想要这辆车,如果是张三先交了订金,那么李四就没有机会了,但是如果李四私下愿意加钱。那么张三和李四之间存在竞争。对于线程来说,我们需要互斥。

生产者VS消费者:互斥、同步

我们假设李四拿到了车,但是张三是个非常执着的人,其他车都不要,就要宝马7系。对于4S店来说,它就应该给工厂发消息生产7系车。然后再告诉张三有车了,进而消费。就对于生产线程和消费线程那就是同步

如果宝马一直疯狂生产,也不管4S店到底卖出去没有,也不管消费者到底买不买,那么这样就乱套了。结局只有破产!!!所以需要根据消费者的需求来进行合理生产。反过来消费者和宝马也是同理。而这对于多线程来说,那就是互斥

1.2 生产消费者模型原则

生产消费者模型原则:321原则

三种关系:

  • 生产者VS生产者:互斥
  • 消费者VS消费者:互斥
  • 生产者VS消费者:同步、互斥

 两种角色:

  • 生产者
  • 消费者

 一个交易场所:

  • 特定的容器:阻塞队列、环形队列 

生产消费者模型原则,书本是没有这个概念,为了方便记忆,大牛提炼总结出来的。

1.3 生产消费者模型的优点 

 为什么生产消费者模型高效?

  • 生产者、消费者 可以在同一个交易场所中进行操作
  • 生产者在生产时,无需关注消费者的状态,只需关注交易场所中是否有空闲位置
  • 消费者在消费时,无需关注生产者的状态,只需关注交易场所中是否有就绪数据
  • 可以根据不同的策略,调整生产者于与消费者间的协同关系

 生产消费者模型可以根据供需关系灵活调整策略做到忙闲不均。生产者和消费者无需关心他人的状态,做到并发。

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

在正式编写代码前,我们先了解阻塞队列与普通的队列区别在于,当队列为空时,从队列获取元素的操作将会被阻塞,直到队列中被放入了元素;当队列满时,往队列里存放元素的操作也会被阻塞,直到有元素被从队列中取出(以上的操作都是基于不同的线程来说的,线程在对阻塞队列进程操作时会被阻塞)

2.1 单生产单消费模型  

为了方便理解我们先用单生产、单消费的方式来讲解

先创建Blockqueue.hpp的头文件。

#include <iostream>
#include <queue>
#include <pthread.h>template <class T>
class Blockqueue
{static const int defaultnum= 10;public:Blockqueue(int maxcap = defaultnum): _maxcap(maxcap){pthread_mutex_init(&_mutex, nullptr);pthread_cond_init(&_c_cond, nullptr);pthread_cond_init(&_p_cond, nullptr);}void push(const T &data) //生产数据{}T pop() //取数据{}~Blockqueue(){pthread_mutex_destroy(&_mutex);pthread_cond_destroy(&_c_cond);pthread_cond_destroy(&_p_cond);}private:std::queue<T> _q;int _maxcap; // 极值pthread_mutex_t _mutex;pthread_cond_t _c_cond; // 消费者pthread_cond_t _p_cond; // 生产者
};

阻塞队列框架搭建出来后生产和消费我们后面实现。

由于我们是单生产单消费的生产消费者模型。所以

mian.cc主函数中创建两个线程

#include "Blockqueue.cpp"void * Consumer(void *args) //消费者
{}
void * Productor(void *args) //生产者
{}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);delete bq;return 0;
}

上面就是生产消费者模型的大致框架,我们在实现具体细节之前,我们先要明白一个关键问题。

生产和消费要不要耗费时间?

生产和消费是肯定要耗费时间的,一辆车不会平白无故的出现,车从生产到成品这个过程是要耗费大量的数据,同理作为消费者使用车,也是要耗费时间的。开车不需要耗费时间吗?

所以在代码层面角度来说:生产和消费都是需要耗费时间的,并不是一味的在阻塞队列里进行生产和消费。而是生产者在生产数据之前,要对数据做加工,做完之后才放进阻塞队列,消费者也不是从阻塞队列拿到数据就完事了,而是拿到数据之后,对数据做分析,然后决策。 

为什么生产和消费只需要同一把锁? 

因为它们两个是基于阻塞队列的,我们可以把阻塞队列看成一份整体资源,所以只需要一把锁,但是共享资源也可以被看做多份。

为什么生产和消费各自需要一个条件变量?

这就是为什么叫做阻塞队列。两个线程各自基于自己的条件变量,当条件不满足时候,那么就会阻塞等待。

明白这点之后 我们来实现生产和消费

生产和消费都能看到同一个阻塞队列,之前我们也说了生产和消费是既有同步又互斥的关系,那么生产线程和消费线程在访问阻塞队列时,只能是只有一个在访问。那么必然要互斥

   void push(const T &data) //生产数据{pthread_mutex_lock(&_mutex);_q.push(data);pthread_mutex_unlock(&_mutex);}

 生产是想生产就能生产的吗?

当然不是,阻塞队列如同超市一样,商品在货架上都放满了,生产出来的商品没有人买,那不是妥妥亏钱?

所以在生产之前还得问问超市,条件满足不?满足生产,不满足堵塞等待被唤醒

   void push(const T &data) //生产数据{pthread_mutex_lock(&_mutex);if(_q.size() == _maxcap) {pthread_cond_wait(&_p_cond,&_mutex);//不满足阻塞}_q.push(data);pthread_cond_signal(&_c_cond);pthread_mutex_unlock(&_mutex);}

当生产条件不满足的时候,那么生产线程要去等待。这里就有个问题,生产线程在访问条件满不满足的时候,是已经拿到了锁的,不释放锁去等待,那么会造成死锁的问题。所以我们利用

pthread_cond_wait函数 ,等待的同时解锁。


同理消费数据也是一样。 

 T pop() //消费数据{pthread_mutex_lock(&_mutex);if(_q.size() == 0) {pthread_cond_wait(&_c_cond,&_mutex);//不满足阻塞}T out = _q.front();_q.pop();pthread_cond_signal(&_p_cond);pthread_mutex_unlock(&_mutex);return out;}

那么我们在实现了生产和消费之后,就需要在mian.cc中实现生产消费的回调函数 

我们先srand函数模拟随机数

srand(time(nullptr) ^ getpid());
#include <ctime>
#include <unistd.h>
void *Consumer(void *args) // 消费者
{Blockqueue<int> *bq = static_cast<Blockqueue<int> *>(args);while (true){int t = bq->pop();std::cout << "消费了一个数据..." << t << std::endl;}
}
void *Productor(void *args) // 生产者
{Blockqueue<int> *bq = static_cast<Blockqueue<int> *>(args);while (true){int data = rand() % 10 + 1;bq->push(data);std::cout << "生产了一个数据..." << data << std::endl;sleep(1);}
}

 结果符合预期,生产和消费实现了同步互斥。但是我们就传入个整数,未免有点锉了,我们是用C++写的,而且我们blockqueue是带模板,我们可以传入对象。

先创建一个Task.hpp的头文件

我们在Task.hpp这个头文件中,创建一个Task类。在这个类中实现一些加减乘除的函数方法,由生产者生产任务。然后消费者拿到任务数据做加工

#pragma once
#include <iostream>
#include <string>std::string opers = "+-*/%";enum
{DivZero = 1,ModZero,Unknown
};class Task
{
public:Task(int data1, int data2, char oper): _data1(data1), _data2(data2), _oper(oper), _result(0), _exitcode(0){}void run(){switch (_oper){case '+':_result = _data1 + _data2;break;case '-':_result = _data1 - _data2;break;case '*':_result = _data1 * _data2;break;case '/':{if (_data2 == 0)_exitcode = DivZero;else_result = _data1 / _data2;}break;case '%':{if (_data2 == 0)_exitcode = ModZero;else_result = _data1 % _data2;}break;default:_exitcode = Unknown;break;}}std::string GetResult(){std::string r = std::to_string(_data1);r += _oper;r += std::to_string(_data2);r += "=";r += std::to_string(_result);r += "[code: ";r += std::to_string(_exitcode);r += "]";return r;}std::string GetTask(){std::string r = std::to_string(_data1);r += _oper;r += std::to_string(_data2);r += "=?";return r;}void operator()() //运算符重载让对象像函数一样使用{run();}~Task(){}private:int _data1;int _data2;char _oper;int _result;int _exitcode;
};

void *Consumer(void *args) // 消费者
{// Blockqueue<int> *bq = static_cast<Blockqueue<int> *>(args);Blockqueue<Task> *bq = static_cast<Blockqueue<Task> *>(args);while (true){Task t = bq->pop();t();std::cout << "处理任务: " << t.GetTask() << " 运算结果是: "<< t.GetResult() << " thread id: " << pthread_self() << std::endl;}
}
void *Productor(void *args) // 生产者
{// Blockqueue<int> *bq = static_cast<Blockqueue<int> *>(args);Blockqueue<Task> *bq = static_cast<Blockqueue<Task> *>(args);int len = opers.size();while (true){int data1 = rand() % 10 + 1;int data2 = rand() % 10;char oper = opers[rand() % len];Task t(data1, data2, oper);bq->push(t);std::cout << "生产了一个任务: " << t.GetTask() << " thread id: " << pthread_self() << std::endl;sleep(1);}
}

注:

 其实我们不用非要等到满了,才停止生产。我们可以定策略,就如同水库的警戒线,当河水上涨到警戒线时,就开闸放水,而不是等到水库满了才放。消费也是同理。

 int low_water_;int high_water_;

2.2 多生产多消费 

我们实现了单生产单消费,这里改成多生产多消费,非常简单。只需要在mian.cc这里循环创建线程即可

int main()
{srand(time(nullptr) ^ getpid());Blockqueue<Task> *bq = new Blockqueue<Task>;// 创建线程(生产、消费)pthread_t c[3], p[5];for (int i = 0; i < 3; i++){pthread_create(c + i, nullptr, Consumer, bq);}for (int i = 0; i < 5; i++){pthread_create(p + i, nullptr, Productor, bq);}for (int i = 0; i < 3; i++){pthread_join(c[i], nullptr);}for (int i = 0; i < 5; i++){pthread_join(p[i], nullptr);}delete bq;return 0;
}

出现上面的错误是因为伪唤醒的原因

为什么会出现伪唤醒的?

现在是多个线程了,也就是说当阻塞队列满时,所有的生产线程被阻塞等待被唤醒。消费线程这时消费一个数据,当阻塞队列不满时,那么就会唤醒所有的生产线程,3个线程只有一个线程能拿到锁,其中一个拿到锁线程进行生产此时阻塞队列已经满了。等其他线程拿到锁后,条件不满足。生产不了,这就是伪唤醒。

所以我们把if改成while 循环判断防止伪唤醒

void push(const T &data) // 生产数据{pthread_mutex_lock(&_mutex);while (_q.size() == _maxcap) // 用while防止伪唤醒,判断条件满不满足{pthread_cond_wait(&_p_cond, &_mutex); // 不满足阻塞}_q.push(data);pthread_cond_signal(&_c_cond);pthread_mutex_unlock(&_mutex);}T pop() // 消费数据{pthread_mutex_lock(&_mutex);while (_q.size() == 0) // 用while防止伪唤醒,判断条件满不满足{pthread_cond_wait(&_c_cond, &_mutex); // 不满足阻塞}T out = _q.front();_q.pop();pthread_cond_signal(&_p_cond);pthread_mutex_unlock(&_mutex);return out;}

 

这里我们直接用C++的锁。

 

std:: mutex _mutex;
void *Consumer(void *args) // 消费者
{// Blockqueue<int> *bq = static_cast<Blockqueue<int> *>(args);Blockqueue<Task> *bq = static_cast<Blockqueue<Task> *>(args);while (true){Task t = bq->pop();t();std::lock_guard<std::mutex> guard(_mutex);std::cout << "处理任务: " << t.GetTask() << " 运算结果是: " << t.GetResult() << " thread id: "<< std::hex  << pthread_self() << std::endl;}
}
void *Productor(void *args) // 生产者
{int len = opers.size();// Blockqueue<int> *bq = static_cast<Blockqueue<int> *>(args);Blockqueue<Task> *bq = static_cast<Blockqueue<Task> *>(args);while (true){   sleep(1);int data1 = rand() % 10 + 1;int data2 = rand() % 10;char oper = opers[rand() % len];Task t(data1, data2, oper);bq->push(t);std::lock_guard<std::mutex> guard(_mutex);std::cout << "生产了一个任务: " << t.GetTask() << " thread id: " << std::hex << pthread_self() << std::endl;}
}

 

为什么只修改线程创建的代码,多线程就能适应原来的消费场景?

原因有2点:

  1. 生产者、消费者都是在对同一个 _queue 操作,用一把锁,保护一个临界资源,足够了
  2. 当前的 _queue 始终是被当作一个整体使用的,无需再增加锁区分

当然也可以让生产者和消费者各自拿一把锁,但是都是基于_queue的完全没有必要,画蛇添足。

3. POSIX 信号量

在 POSIX 标准中,信号量(semaphore)是一种用于控制多个进程或线程对共享资源访问的同步机制。信号量是一个计数器,它可以跟踪一定数量的资源或信号量单位。进程或线程可以通过原子操作对信号量进行增加或减少,从而实现对共享资源的协调访问。 

也就是说,让线程的同步的方法,不仅仅只有条件变量,还有信号量。 

POSIX 信号量有两种类型:

  1. 无名信号量(Unnamed semaphores):也称为进程间信号量,因为它们可以在不同的进程之间共享。无名信号量使用 sem_t 类型表示,并通过 sem_init() 函数初始化,使用 sem_destroy() 函数销毁。无名信号量需要一个与之关联的键值来标识,这个键值可以通过 ftok()shmget() 函数获得。

  2. 命名信号量(Named semaphores):也称为系统V信号量,它们是系统范围内唯一的,并且可以跨会话使用。命名信号量通过 semget() 函数创建,使用 semctl() 函数控制,使用 semop() 函数进行操作。

文档的话太抽象了,下面我用大白话来解释信号量

我们将阻塞队列比喻成电影院,而信号量就如同电影票,电影院是一个整体的公共资源,那么电影院的座位就把电影院这个整体划分为无数份的资源。而信号量就是预定座位资源。 

那么当我们购买电影票成功或不成功,对应编程来说,其实就是在访问临界资源的同时进行了临界资源就绪或者不就绪判断。

就绪意味者线程可以访问

不就绪意味着线程不可访问

POSIX 信号量的基本操作:

初始化:使用 sem_init() 初始化一个无名信号量

int sem_init(sem_t *sem, int pshared, unsigned int value);
  • sem:指向信号量变量的指针。
  • pshared:非零表示信号量可以被其他进程访问,零表示只能在当前进程内访问。
  • value:信号量的初始值。

 等待(减):使用 sem_wait()sem_trywait() 减少信号量,如果信号量的值大于零,则减少其值,否则进程将等待。

int sem_wait(sem_t *sem);
int sem_trywait(sem_t *sem);

信号量值增加(信号):使用 sem_post() 增加信号量的值,如果其他进程因为信号量的值小于或等于零而等待,则其中一个进程将被唤醒。 

int sem_post(sem_t *sem);

获取信号量值:使用 sem_getvalue() 获取信号量的当前值。 

int sem_getvalue(sem_t *sem, int *sval);

销毁信号量:使用 sem_destroy() 销毁一个无名信号量。 

int sem_destroy(sem_t *sem);

   这些接口使用起来还是比较简单,下面我们用信号量来实现生产消费者模型。前面用的是阻塞队列,我们用信号量实现基于循环队列版本。

4. 基于循环队列实现生产消费者模型

在实现之前我们先了解循环队列这种数据结构。我们利用数组这种数据结构,然后对下标进行取模可以让数组变成循环的结构 

 

一张动图搞定循环队列这种数据结构

这里有几个关键问题:

问题1:生产者关注什么资源?消费者关注什么资源?

生产者关注的是数组还有多少空间、消费者关注的是数组还有多少数据。 

问题2:生产者和消费者什么时候才会指向同一个位置?

要么数组为空、要么数组为满。(这两种状态只能是生产和消费其中一个进行访问,空生产者访问、满消费者访问。)

反之一定是指向不同的位置 (这句话非常重要,意味着生产和消费可以同时访问)

那么循环队列要正常运行必须满足3个条件

1. 空或者满只能有一个人访问

2. 消费者一定不能超过生产者

3. 生产者一定不能套圈消费者 

如果消费者超过生产者,前面都没有数据,访问什么?

为什么这么说?因为最开始一定为空。那么一定是生产者先走!毫无疑问

如果生产者套圈消费者意味着生产速度大于消费速度之前没有消费的数据要被覆盖。数据出现覆盖,严重错误。 

 理解了这些问题我们直接多生产多消费来实现

4.1 多生产多消费

老规矩先创建RingQueue.hpp头文件 

#include <iostream>
#include <vector>
#include <pthread.h>
#include <semaphore.h>
const static int defaultcap = 5;
template <class T>
class RingQueue
{
public:RingQueue(int cap = defaultcap): _ringqueue(cap), _cap(cap), _c_step(0), _p_step(0)sem_init(&_cdata_sem, 0, 0);sem_init(&_pspace_sem, 0, cap);pthread_mutex_init(&_c_mutex, nullptr);pthread_mutex_init(&_p_mutex, nullptr);}void push(const T& data){}T pop(T* out){}~RingQueue(){sem_destroy(&_cdata_sem);sem_destroy(&_pspace_sem);pthread_mutex_destroy(&_c_mutex);pthread_mutex_destroy(&_p_mutex);}private:std::vector<T> _ringqueue; // 循环队列int _cap;                  // 循环队列容量int _c_step;               // 消费者下标int _p_step;               // 生产者下标sem_t _cdata_sem;          // 消费者关注的数据资源sem_t _pspace_sem;         // 生产者关注的空间资源pthread_mutex_t _c_mutex;  // 消费者锁pthread_mutex_t _p_mutex;  // 生产者锁
};

框架大致构建出来,为了方便生产消费的互斥与同步。我们接下来对生产和消费线程互斥与同步的函数进行封装 

void Lock(pthread_mutex_t &mutex){pthread_mutex_lock(&mutex);}void UnLock(pthread_mutex_t &mutex){pthread_mutex_unlock(&mutex);}void P(sem_t &sem) //减少{sem_wait(&sem);}void v(sem_t &sem) //增加{sem_post(&sem);}

实现push 和 pop函数 

 void Push(const T &data){P(_pspace_sem);Lock(_p_mutex);_ringqueue[_p_step++] = data;_p_step %= _cap;UnLock(_p_mutex);V(_cdata_sem);}T Pop(T *out){P(_cdata_sem);Lock(_c_mutex);*out = _ringqueue[_c_step++];_c_step %= _cap;Unlock(_c_mutex);V(_pspace_sem);return out;}

 这里解释push函数P操作为什么传入的是空间信号量,很简单生产者关注的是空间资源,所以这里P判断空间资源就不就绪,V为什么传入的是数据信号量?当P申请成功意味着可以生产,那么对应空间资源减少,数据资源增加。

同理pop也是一样。

我们mian.cc创建线程 和回调函数

#include <unistd.h>
#include <mutex>
#include <ctime>
#include "RingQueue.hpp"
#include "Task.hpp"
std::mutex _mutex;
void *consumer(void *args)
{RingQueue<Task> *rq = static_cast<RingQueue<Task> *>(args);while (true){Task t;rq->Pop(&t);t();std::lock_guard<std::mutex> guard(_mutex);std::cout << "处理任务: " << t.GetTask() << " 运算结果是: " << t.GetResult() << " thread id: "<< std::hex  << pthread_self() << std::endl;}
}
void *productor(void *args)
{RingQueue<Task> *rq = static_cast<RingQueue<Task> *>(args);int len = opers.size();while (true){sleep(1);int data1 = rand() % 10 + 1;int data2 = rand() % 10;char oper = opers[rand() % len];Task t(data1, data2, oper);rq->Push(t);std::lock_guard<std::mutex> guard(_mutex);std::cout << "生产了一个任务: " << t.GetTask() << " thread id: " << std::hex << pthread_self() << std::endl;}
}int main()
{srand(time(nullptr) ^ getpid()); // 随机数种子RingQueue<Task> *rq = new RingQueue<Task>(40);pthread_t c[3], p[3];for (int i = 0; i < 3; i++){pthread_create(c + i, nullptr, consumer, rq);}for (int i = 0; i < 3; i++){pthread_create(p + i, nullptr, productor, rq);}for (int i = 0; i < 3; i++){pthread_join(c[i], nullptr);}for (int i = 0; i < 3; i++){pthread_join(p[i], nullptr);}delete rq;return 0;
}

 这里打印只打印了线程ID,我们可以重新创建一个线程名字的类。把线程名字加入进去

struct ThreadData
{RingQueue<Task> *rq;std::string threadname;
};

细节: 加锁行为放在信号量申请成功之后,可以提高并发度

 

为什么这么说,信号量在加锁之前就好比,没进电影院之前就已经选好了座位,如果在加锁之后,那就如同进到电影院之后在选座位,而再选座位就又得排队买票。而且信号量本身就是原子操作

那既然阻塞队列也能实现生产消费者模型,那搞出来个循坏队列又有什么用?

环形队列的优缺点:

优点

  1. 空间利用率高:由于是环形结构,已使用的空间可以重复利用,不会像普通队列一样造成空间的浪费。
  2. 插入和删除速度快:由于是线性结构,环形队列的插入和删除操作通常很快,因为它们只涉及到头尾指针的移动。
  3. 固定大小的存储空间:可以避免内存泄漏等问题,因为不会动态地分配和回收内存。

缺点

  1. 需要额外的指针维护状态:增加了复杂度,需要维护队列头和队尾的指针。
  2. 存储空间可能未被充分利用:一旦队列满了,就需要覆盖队列头的元素,这可能导致存储空间没有被完全利用。
  3. 队列大小必须预先定义:难以动态调整大小,这在某些需要灵活内存使用的场景下可能是一个限制。

阻塞队列的优缺点:

优点

  1. 线程同步:阻塞队列可以很好地实现线程之间的同步,简化了生产者和消费者之间的数据传递和通信。
  2. 解耦合:作为生产者消费者模式的缓冲空间,阻塞队列降低了生产者和消费者之间的耦合性。
  3. 削峰填谷:由于阻塞队列的大小是有限的,它可以起到限制作用,平衡突发的流量高峰。

缺点

  1. 可能引发死锁:如果使用不当,比如生产者和消费者互相等待对方释放资源时,可能会发生死锁。
  2. 对性能的影响:线程的挂起和唤醒操作可能会对系统性能产生影响,尤其是在高并发场景下。
  3. 处理超时操作较复杂:在设置了超时时间的情况下,需要处理超时异常并进行相应的补偿或回滚操作,增加了编程复杂性。

每种数据结构都有其特定的使用场景和限制,开发者在选择时应根据具体需求和上下文来决定使用哪一种。

本篇我们学习了什么是生产消费者模型,基于两种数据结构,分别实现了生产消费者模型,

还掌握了一个线程同步神奇——信号量。这对于提高线程之间的并发度非常有用。再次理解了生产消费者模型为什么高效?总之生产消费者模型非常值得我们学习。

 


 

相关文章:

Linux 生产消费者模型

&#x1f493;博主CSDN主页:麻辣韭菜&#x1f493;   ⏩专栏分类&#xff1a;Linux初窥门径⏪   &#x1f69a;代码仓库:Linux代码练习&#x1f69a;   &#x1f339;关注我&#x1faf5;带你学习更多Linux知识   &#x1f51d; 前言 1. 生产消费者模型 1.1 什么是生产消…...

深入浅出:MongoDB中的背景创建索引

深入浅出&#xff1a;MongoDB中的背景创建索引 想象一下&#xff0c;你正忙于将成千上万的数据塞入你的MongoDB数据库中&#xff0c;你的用户期待着实时的响应速度。此时&#xff0c;你突然想到&#xff1a;“嘿&#xff0c;我应该给这些查询加个索引&#xff01;” 没错&…...

Spring事务十种失效场景

首先我们要明白什么是事务&#xff1f;它的作用是什么&#xff1f;它在什么场景下在Spring框架下会失效&#xff1f; 事务&#xff1a;本质上是由数据库和程序之间交互的过程中的衍生物,它是一种控制数据的行为规则。有几个特性 1、原子性&#xff1a;执行单元内&#xff0c;要…...

JELR-630HS漏电继电器 30-500mA 导轨安装 约瑟JOSEF

JELR-HS系列 漏电继电器型号&#xff1a; JELR-15HS漏电继电器&#xff1b;JELR-25HS漏电继电器&#xff1b; JELR-32HS漏电继电器&#xff1b;JELR-63HS漏电继电器&#xff1b; JELR-100HS漏电继电器&#xff1b;JELR-120HS漏电继电器&#xff1b; JELR-160HS漏电继电器&a…...

如何实现一个简单的链表或栈结构

实现一个简单的链表或栈结构是面向对象编程中的基础任务。下面我将分别给出链表和栈的简单实现。 链表&#xff08;单链表&#xff09;的实现 链表是由一系列节点组成的集合&#xff0c;每个节点都包含数据部分和指向列表中下一个节点的链接&#xff08;指针或引用&#xff0…...

抖音外卖服务商入驻流程及费用分别是什么?入驻官方平台的难度大吗?

随着抖音关于新增《【到家外卖】内容服务商开放准入公告》的意见征集通知&#xff08;以下简称“通知”&#xff09;的发布&#xff0c;抖音外卖服务商入驻流程及费用逐渐成为众多创业者所关注和热议的话题。不过&#xff0c;就当前的讨论情况来看&#xff0c;这个话题似乎没有…...

“小红书、B站崩了”,背后的阿里云怎么了?

导语&#xff1a;阿里云不能承受之重 文 | 魏强 7月2日&#xff0c;“小红书崩了”、“B站崩了”等话题登上了热搜。 据第一财经、财联社等报道&#xff0c;7月2日&#xff0c;用户在B站App无法使用浏览历史关注等内容&#xff0c;消息界面、更新界面、客服界面均不可用&…...

nginx的配置文件

nginx.conf 1、全局模块 worker_processes 1; 工作进程数&#xff0c;设置成服务器内核数的2倍&#xff08;一般不超过8个&#xff0c;超过8个反正会降低性能&#xff0c;4个 1-2个 &#xff09; 处理进程的过程必然涉及配置文件和展示页面&#xff0c;也就是涉及打开文件的…...

艾滋病隐球菌病的病原学诊断方法包括?

艾滋病隐球菌病的病原学诊断方法包括()查看答案 A.培养B.隐球菌抗原C.墨汁染色D.PCR 在感染性疾病研究中&#xff0c;单细胞转录组学的应用包括哪些()? A.细胞异质性研究B.基因组突变检测C.感染过程单细胞分析D.代谢通路分析 开展病原微生物网络实验室体系建设&#xff0c;应通…...

jQuery Tooltip 插件使用教程

jQuery Tooltip 插件使用教程 引言 jQuery Tooltip 插件是 jQuery UI 套件的一部分,它为网页元素添加了交互式的提示框功能。通过这个插件,开发者可以轻松地为链接、按钮、图片等元素添加自定义的提示信息,从而增强用户的交互体验。本文将详细介绍如何使用 jQuery Tooltip…...

访问者模式在金融业务中的应用及其框架实现

引言 访问者模式&#xff08;Visitor Pattern&#xff09;是一种行为设计模式&#xff0c;它允许你在不改变对象结构的前提下定义作用于这些对象的新操作。通过使用访问者模式&#xff0c;可以将相关操作分离到访问者中&#xff0c;从而提高系统的灵活性和可维护性。在金融业务…...

.npy格式图像如何进行深度学习模型训练处理,亲测可行

import torchimport torch.nn as nnimport torch.nn.functional as Fimport numpy as npfrom torch.utils.data import DataLoader, Datasetfrom torchvision import transformsfrom PIL import Imageimport json# 加载训练集和测试集数据train_images np.load(../dataset/tra…...

XFeat快速图像特征匹配算法

XFeat&#xff08;Accelerated Features&#xff09;是一种新颖的卷积神经网络&#xff08;CNN&#xff09;架构&#xff0c;专为快速和鲁棒的像匹配而设计。它特别适用于资源受限的设备&#xff0c;同时提供了与现有深度学习方法相比的高速度和准确性。 轻量级CNN架构&#xf…...

普元EOS学习笔记-低开实现图书的增删改查

前言 在前一篇《普元EOS学习笔记-创建精简应用》中&#xff0c;我已经创建了EOS精简应用。 我之前说过&#xff0c;EOS精简应用就是自己创建的EOS精简版&#xff0c;该项目中&#xff0c;开发者可以进行低代码开发&#xff0c;也可以进行高代码开发。 本文我就记录一下自己在…...

动态住宅代理IP详细解析

在大数据时代的背景下&#xff0c;代理IP成为了很多企业顺利开展的重要工具。代理IP地址可以分为住宅代理IP地址和数据中心代理IP地址。选择住宅代理IP的好处是可以实现真正的高匿名性&#xff0c;而使用数据中心代理IP可能会暴露自己使用代理的情况。 住宅代理IP是指互联网服务…...

等保2.0 实施方案之信息软件验证要求

一、等保2.0背景及意义 随着信息技术的快速发展和网络安全威胁的不断演变&#xff0c;网络安全已成为国家安全、社会稳定和经济发展的重要保障。等保2.0&#xff08;即《信息安全技术 网络安全等级保护基本要求》2.0版本&#xff09;作为网络安全等级保护制度的最新标准&#x…...

【LeetCode的使用方法】

🎥博主:程序员不想YY啊 💫CSDN优质创作者,CSDN实力新星,CSDN博客专家 🤗点赞🎈收藏⭐再看💫养成习惯 ✨希望本文对您有所裨益,如有不足之处,欢迎在评论区提出指正,让我们共同学习、交流进步! 🔮LeetCode的使用方法 🔮LeetCode 是一个在线编程平台,广泛…...

【SGX系列教程】(二)第一个 SGX 程序: HelloWorld,linux下运行

文章目录 0. SGX基础原理分析一.准备工作1.1 前提条件1.2 SGX IDE1.3 基本原理 二.程序设计2.1 目录结构2.2 源码设计2.2.1 Encalve/Enclave.edl:Enclave Description Language2.2.2 Enclave/Enclave.lds: Enclave linker script2.2.3 Enclave/Enclave.config.xml: Enclave 配置…...

网页报错dns_probe_possible 怎么办?——错误代码有效修复

当你在浏览网页时遇到dns_probe_possible 错误&#xff0c;这通常意味着你的浏览器无法解析域名系统&#xff08;DNS&#xff09;地址。这个问题可能是由多种原因引起的&#xff0c;包括网络配置问题、DNS服务问题、或是本地设备的问题。教大家几种修复网页报错dns_probe_possi…...

Vue.js 中属性绑定的详细解析:冒号 `:` 和非冒号的区别

Vue.js 中属性绑定的详细解析&#xff1a;冒号 : 和非冒号的区别 在 Vue.js 中&#xff0c;属性绑定是一个重要的概念&#xff0c;它决定了如何将数据绑定到 DOM 元素的属性上。Vue.js 提供了两种方式来绑定属性&#xff1a;使用冒号 : 进行动态绑定&#xff0c;或直接书写属性…...

使用Java实现智能物流管理系统

使用Java实现智能物流管理系统 大家好&#xff0c;我是免费搭建查券返利机器人省钱赚佣金就用微赚淘客系统3.0的小编&#xff0c;也是冬天不穿秋裤&#xff0c;天冷也要风度的程序猿&#xff01;今天我们将探讨如何使用Java语言实现智能物流管理系统&#xff0c;这是一个涉及到…...

深圳技术大学oj C : 生成r子集

Description 输出给定序列按字典序的 &#xfffd; 组合&#xff0c;按照所有 &#xfffd; 个元素出现与否的 01 标记串 &#xfffd;&#xfffd;&#xfffd;&#xfffd;−1,...,&#xfffd;1 的字典序输出. 此处01串的字典序指&#xff1a;先输入的数字对应低位&#x…...

不同操作系统下的换行符

1. 关键字2. 换行符的比较3. ASCII码4. 修改换行符 4.1. VSCode 5. 参考文档 1. 关键字 CR LF CRLF 换行符 2. 换行符的比较 英文全称英文缩写中文含义转义字符ASCII码值操作系统Carriage ReturnCR回车\r13MacIntosh&#xff08;早期的Mac&#xff09;LinefeedLF换行/新行\…...

Transformation(转换)开发-switch/case组件

一、switch/case组件-条件判断 体育老师要做一件非常重要的事情&#xff1a;判断学生是男孩还是女孩、或者是蜘蛛&#xff0c;然后让他们各自到指定的队伍中 体育老师做的事情&#xff0c;我们同样也会在Kettle中会经常用来。在Kettle中&#xff0c;switch/case组件可以来做类似…...

Android Gradle 开发与应用 (二): Android 项目结构与构建配置

目录 1. Android 项目的 Gradle 文件结构 1.1 项目根目录 1.2 模块目录 2. Gradle 构建配置详解 2.1 配置 Android 项目的 build.gradle 2.2 配置模块的 build.gradle 2.3 使用 productFlavors 管理多版本应用 2.4 使用 buildConfigField 注入构建常量 在 Android 开发…...

02:vim的使用和权限管控

vim的使用 1、vim基础使用1.1、vim pathname 2、vim高级用法2.1、查找2.2、设置显示行号2.3、快速切换行2.4、 行删除2.5、行复制粘贴 3、权限管理3.1、普通用户和特权用户3.2、文件权限表示 vim是Linux中的一种编辑器&#xff0c;类似于window中的记事本&#xff0c;可以对创建…...

GNeRF代码复现

https://github.com/quan-meng/gnerf 之前一直去复现这个代码总是文件不存在&#xff0c;我就懒得搞了&#xff08;实际上是没能力哈哈哈&#xff09; 最近突然想到这篇论文重新试试复现 一、按步骤创建虚拟环境安装各种依赖等 二、安装好之后下载数据&#xff0c;可以用Blen…...

EXCEL返回未使用数组元素(未使用值)

功能简介&#xff1a; 在我们工作中&#xff0c;需要在EXCEL表列出哪些元素&#xff08;物品或订单&#xff09;已经被使用了&#xff08;或使用了多少次&#xff09;&#xff0c;哪些没有被使用。 当数量过于庞大时人工筛选或许不是好办法&#xff0c;我们可以借助公式&…...

系统调用简单介绍

概述 简单理解就是操作系统给我们提供的函数接口&#xff0c;当我们的程序需要执行一些只有操作系统才能完成的工作的时候&#xff0c;我们就要调用操作系统给我们提供的接口来实现这些功能&#xff0c;这些接口就是系统调用。 那什么样的操作是只有操作系统才能完成呢? 比如…...

Mac可以读取NTFS吗 Mac NTFS软件哪个好 mac ntfs读写工具免费

在跨操作系统环境下使用外部存储设备时&#xff0c;特别是当Windows系统的U盘被连接到Mac电脑时&#xff0c;常常会遇到文件系统兼容性的问题。由于Mac OS原生并不完全支持对NTFS格式磁盘的读写操作&#xff0c;导致用户无法直接在Mac上向NTFS格式的U盘或硬盘写入数据。下面我们…...

AI是否能够做决定

AI是在帮助开发者还是取代他们&#xff1f; 我认为AI功能虽然很强大&#xff0c;但是代替不了人&#xff0c;原因就在于人可以做决定&#xff0c;可以承担责任和后果&#xff0c;但是AI不能够为结果负责...

【Excel操作】Python Pandas判断Excel单元格中数值是否为空

判断Excel单元格中数值是为空&#xff0c;主要有下面两种方法&#xff1a; 1. pandas.isnull 2. pandas.isna判断Excel不为空&#xff0c;也有下面两种方法&#xff1a; 1. pandas.notna 2. pandas.notnull假设有这样一张Excel的表格 我们来识别出为空的单元格 import panda…...

C# Opacity 不透明度

WinForms Opacity以下是一些使用 Opacity 属性的示例&#xff1a;设置窗体的透明度&#xff1a;设置按钮的透明度&#xff1a;动态改变控件的透明度&#xff1a;使用定时器改变透明度&#xff1a;在窗体加载时设置透明度&#xff1a; 请注意另外 WPF Opacity以下是一些使用 Opa…...

推荐三款常用接口测试工具!

接口测试是软件开发中至关重要的一环&#xff0c;通过对应用程序接口进行测试&#xff0c;可以验证其功能、性能和稳定性。随着互联网和移动应用的快速发展&#xff0c;接口测试变得越来越重要。为了提高测试效率和质量&#xff0c;开发人员和测试人员需要使用专业的接口测试工…...

【Qt】Qt多线程编程指南:提升应用性能与用户体验

文章目录 前言1. Qt 多线程概述2. QThread 常用 API3. 使用线程4. 多线的使用场景5. 线程安全问题5.1. 加锁5.2. QReadWriteLocker、QReadLocker、QWriteLocker 6. 条件变量 与 信号量6.1. 条件变量6.2 信号量 总结 前言 在现代软件开发中&#xff0c;多线程编程已成为一个不可…...

PyTorch之nn.Module、nn.Sequential、nn.ModuleList使用详解

文章目录 1. nn.Module1.1 基本使用1.2 常用函数1.2.1 核心函数1.2.2 查看函数1.2.3 设置函数1.2.4 注册函数1.2.5 转换函数1.2.6 加载函数 2. nn.Sequential()2.1 基本定义2.2 Sequential类不同的实现2.3 nn.Sequential()的本质作用 3. nn.ModuleList参考资料 本篇文章主要介绍…...

C++Primer Plus 第十四章代码重用:编程练习,第4题

CPrimer Plus 第十四章代码重用&#xff1a;编程练习,第4题 CPrimer Plus 第十四章代码重用&#xff1a;编程练习,第4题 文章目录 CPrimer Plus 第十四章代码重用&#xff1a;编程练习,第4题前言4.一、定义二、方法 前言 4. Person 类保存人的名和姓。除构造函数外&#xff…...

01 Docker 概述

目录 1.Docker简介 2.传统虚拟机 vs 容器 3.Docker运行速度快的原因 4.Docker基本组成三要素 5.Docker 平台架构 入门版 架构版 1.Docker简介 Docker是基于Go语言实现的云开源项目。 Docker的主要目标是&#xff1a;Build, Ship and Run Any App, Anywhere&#xff0c…...

c++的const

const在C中是一个非常重要的关键字&#xff0c;用于定义不可变的变量、函数参数、成员函数等。它可以提高代码的可读性、安全性&#xff0c;并帮助编译器进行优化。 定义常量 使用const定义不可变的变量&#xff1a; const int MAX_SIZE 100;常量指针 指向常量的指针和常量…...

Git不想跟踪某个文件

如果你不想跟踪某个文件&#xff0c;可以将该文件路径添加到 .gitignore 文件中。.gitignore 文件用于告诉 Git 哪些文件或目录应该被忽略&#xff0c;不进行版本控制。以下是具体步骤&#xff1a; 编辑 .gitignore 文件&#xff1a;在项目的根目录下找到或创建一个 .gitignore…...

DB-GPT 文档切分报错

感谢阅读 配置完知识库&#xff0c;进行切分报错切分完成后&#xff0c;进行问答时后台日志报错 配置完知识库&#xff0c;进行切分报错 报的错如下 document sync error cryptography>3.1 is required for AES algorithm pip install -U cryptography 之后重新运行程序 …...

#如何使用 Qt 5.6 在 Android 上启用 NFC

如何使用 Qt 5.6 在 Android 上启用 NFC NFC 技术在 Android 应用开发中变得越来越重要。在本文中&#xff0c;我将介绍如何使用 Qt 5.6 来实现 Android 上的 NFC 功能。这个教程基于一个创建于 8 年 8 个月前的问题&#xff0c;并在 7 年 3 个月前进行了修改&#xff0c;讨论…...

合并排序的数组

题目链接 合并排序的数组 题目描述 注意点 A的末端有足够的缓冲空间容纳BA和B都是排序的 解答思路 最初想到的是双指针&#xff0c;从小到大找到合并B时应该A相应位置应该插入的元素&#xff0c;因为在插入的过程中B的元素会替换A原有位置的元素&#xff0c;所以需要先将A…...

js 复制文本带样式

一键复制带样式的html文本到邮件 <div><div idcopy-content><div style{{ fontSize: 16px,fontWeight: 500, lineHeight: 24px, color: #222, marginBottom: 16px }}>邀请您参加腾讯会议网络研讨会&#xff08;Webinar)</div></div><Button …...

服务器之BIOS基础知识总结

1.BIOS是什么&#xff1f; BIOS全称Basic Input Output System&#xff0c;即基本输入输出系统&#xff0c;是固化在服务器主板的专用ROM上&#xff0c;加载在服务器硬件系统上最基本的运行程序&#xff0c;它位于服务器硬件和OS之间&#xff0c;在服务器启动过程中首先运行&am…...

FFmpeg 实现从摄像头获取流并通过RTMP推流

使用FFmpeg库实现从USB摄像头获取流并通过RTMP推流&#xff0c;FFmpeg版本为4.4.2-0。RTMP服务器使用的是SRS&#xff0c;拉流端使用VLC。 在Linux上查看摄像头信息可使用 v4l2-ctl 工具&#xff0c;查看命令如下&#xff1a; v4l2-ctl --device/dev/video0 --list-formats-e…...

学生管理系统

一、登录 用户类&#xff1a;属性&#xff1a;用户名、密码、身份证号码、手机号码 1、欢迎页面 System.out.println("欢迎来到学生管理系统"); System.out.println("请选择操作1登录 2注册 3忘记密码"); 代码实现&#xff1a; //欢迎页面public static…...

【linux】网络基础(3)——tcp协议

文章目录 TCP协议概括TCP头部格式TCP连接管理建立连接&#xff08;三次握手&#xff09;数据传输确认应答机制捎带应答 滑动窗口丢包问题 拥塞控制延时应达 终止连接&#xff08;四次挥手&#xff09; TCP协议概括 TCP是一个面向连接的协议&#xff0c;在传输数据之前需要建立连…...

[Day 21] 區塊鏈與人工智能的聯動應用:理論、技術與實踐

區塊鏈的智能合約運行機制 區塊鏈技術自比特幣誕生以來&#xff0c;便以其去中心化、安全性和透明性等特點引起了廣泛的關注和應用。而智能合約作為區塊鏈技術的一大創新&#xff0c;進一步擴展了區塊鏈的應用場景&#xff0c;使其不僅僅局限於數字貨幣&#xff0c;還可以應用…...

使用ps给gif动图抠图

目录 导入gif图片 打开时间轴 选择图片 魔棒抠图-初步抠图 套索抠图-精准抠图 导入gif图片 打开时间轴 因为gif动图实际上多张图片实现的效果&#xff0c;所以如果要给gif抠图&#xff0c;就得挨个给每个时间线的图片抠图 点击窗口->时间轴 选择图片 在时间轴上选择要…...

谈谈JVM内存区域的划分,哪些区域可能发生OutOfMemoryError?(jvm)

Java 虚拟机&#xff08;JVM&#xff09;在运行时将内存划分为多个区域&#xff0c;以便有效管理和分配内存资源。这些内存区域包括堆&#xff08;Heap&#xff09;、方法区&#xff08;Method Area&#xff09;、程序计数器&#xff08;Program Counter Register&#xff09;、…...

如何使用Xcode查看iOS APP客户端日志

在测试iOS app过程中&#xff0c;能够有效查看和分析客户端日志是至关重要的。不论是定位crash还是解决复杂的逻辑错误&#xff0c;日志都扮演了不可或缺的角色。Apple的Xcode提供了一个强大的工具集&#xff0c;帮助测试同学有效地进行日志查看和分析。本文将逐步指导如何使用…...

ABB PPC902AE1013BHE010751R0101控制器 处理器 模块

ABB PPC902AE1013BHE010751R0101 该模块是用于自动化和控制系统的高性能可编程控制器。它旨在与其他自动化和控制设备一起使用&#xff0c;以提供完整的系统解决方案 是一种数字输入/输出模块&#xff0c;提供了高水平的性能和可靠性。它专为苛刻的工业应用而设计&#xff0c…...

十大排序:插入/希尔/选择/堆/冒泡/快速/归并/计数/基数/桶排序 汇总(C语言)

目录 前言非线性时间比较类插入排序(1) 直接插入排序(2) 希尔排序 选择排序(3) 选择排序优化版(4) 堆排序 交换排序(5) 冒泡排序(6) 快速排序hoare版本挖坑版前后指针版非递归版 归并排序(7) 归并排序递归版非递归版 线性时间比较类(8) 计数排序基数排序与桶排序 总结 前言 在计…...

ORB 特征点提取

FAST关键点 选取像素p&#xff0c;假设它的亮度为Ip&#xff1b; . 设置一个阈值T&#xff08;比如Ip的20%&#xff09;&#xff1b; 以像素p为中心&#xff0c;选取半径为3的圆上的16个像素点&#xff1b; 假如选取的圆上&#xff0c;有连续的N个点的亮度大于IpT或小于…...

【算法笔记自学】入门篇(2)——算法初步

4.1排序 自己写的题解 #include <stdio.h> #include <stdlib.h>void selectSort(int A[], int n) {for(int i 0; i < n - 1; i) { // 修正索引范围int k i;for(int j i 1; j < n; j) { // 修正索引范围if(A[j] < A[k]) {k j;}}if (k ! i) { // 仅在…...

考拉五座北京车展惊艳亮相,黑科技再次成为制胜法宝

在刚刚结束的北京国际车展上,极狐品牌携旗下全系车型强势登场,其中考拉五座更是首次亮相,引起了广泛关注。这款车型的出现,预示着智能汽车领域又迎来了一位实力不俗的新成员。随着考拉五座的加入,新能源汽车市场的竞争无疑将更加激烈和多彩。车展上,观众们纷纷驻足观看,…...

产品力拉满的领克07EM-P,誓做20万内最强的混动轿车

在刚刚过去的4月,领克品牌销量18727辆,同比增长34%。在竞争激烈的国内汽车市场中,领克品牌能够取得这样的成绩着实不易。凭借EM-P超级增程电动方案、多维智联的智能座舱以及全域安全设计理念,领克07 EM-P在成为细分市场标杆车型的同时,将成为领克品牌新的销量助推剂,帮助…...

亲子运动会家长爆发超强胜负欲 儿子不慌不忙 爸爸累出表情包

...

科学技术创新杂志科学技术创新杂志社科学技术创新编辑部2024年第10期目录

科技创新 单桩穿越岩溶发育地层力学特征与溶洞处置措施研究 刘飞; 1-7《科学技术创新》投稿&#xff1a;cnqikantg126.com 基于多目标优化的中低压配电网电力规划研究 向星山;杨承俊;张寒月; 8-11 激光雷达测绘技术在工程测绘中的应用研究 张军伟;闫宏昌; 12-15 …...

量化交易:如何在QMT中运行Python策略并在VSCode中高效调试?

哈喽&#xff0c;大家好&#xff0c;我是木头左&#xff01; 为何选择QMT和VSCode进行量化策略开发&#xff1f; 在量化交易的世界里&#xff0c;选择正确的工具与拥有优秀的策略同等重要。调用用Visual Studio Code&#xff08;简称VSCode&#xff09;或pycharm&#xff0c;方…...

The Sandbox 和 Bitkub 联手增强东南亚元宇宙中心

作为去中心化游戏虚拟世界和区块链平台的先驱&#xff0c;The Sandbox 正与泰国领先的区块链网络 Bitkub Blockchain Technology Co., Ltd. 展开创新合作。双方合作的目的是将Bitkub元宇宙的影响力扩展到The Sandbox&#xff0c;建立一个元宇宙中心&#xff0c;向用户承诺从 Bi…...