生产者消费者模型

生产者消费者模型

位图 (9)

文章目录

  • 生产者消费者模型
      • 概念
      • 原则
      • 优点
    • 基于BlockingQueue的生产者消费者模型
      • BlockingQueue
      • 模拟实现单生产者消费者模型
      • 基于计算任务和存储任务的生产者消费者模型

概念

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

原则

实际上生产者消费者模型本质上是维护321原则

生产者消费者模型是多线程互斥与同步的经典场景,通常有以下原则:

  • 三种关系:生产者与生产者(互斥关系)、消费者与消费者(互斥关系)、生产者与消费者(互斥和同步关系)
  • 两种角色:生产者和消费者(通常由线程或进程充当)
  • 一个交易场所:通常指的是一个特定的缓冲区,临时保存数据的场所
  • 在生产者之间或消费者之间,阻塞队列作为公共资源要被多个线程访问,那么就要被互斥量保护起来即作为临界资源。而互斥锁需要被多个线程竞争式申请,对于生产者而言,一次只允许一个生产者访问临界资源即生产者之间具有互斥关系;对于消费者而言,一次只允许一个消费者访问临界资源即消费者之间具有互斥关系;
  • 若生产者一直生产,直到缓冲区满了则生产失败,而消费者一直消费,直到缓冲区为空则消费失败。一边一直占用锁导致另一边的饥饿问题,这是非常低效的。因此在生产者和消费者之间,阻塞队列也需要被互斥量保护起来即是临界资源,生产者和消费者不能同时访问即二者具有互斥关系,而生产者和消费者需要具有一定的顺序访问即具有同步关系

image-20230720154156259

优点

  • 解耦
  • 支持并发
  • 支持忙先不均
  • 我们在主函数调用目标函数实际上是强耦合,主函数将参数传参给目标函数,主函数传递了数据作为生产者,形成变量即变量暂时保存了数据,目标函数将该变量进行操作并返回,即目标函数消费了数据作为消费者,而主函数需要等待接收目标函数的返回值才能往下执行,因此主函数和目标函数是强耦合关系。
  • 而生产者消费者模型中,生产者向缓冲区中生产数据,若缓冲区没满则可以一直生产;消费者可以一直从缓冲区里取数据,若缓冲区不为空则可以一直消费;那么生产者和消费者就可以并发执行,即生产者消费者模型是对强耦合关系的解耦。

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

BlockingQueue

在多线程编程中阻塞队列(Blocking Queue)是一种常用于实现生产者和消费者模型的数据结构。

image-20230720160951424

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

模拟实现单生产者消费者模型

为了方便理解,下面以单生产者消费者为例子

image-20230720162834965

main.cc

#include<iostream>
#include<pthread.h>
#include<assert.h>
#include<queue>
#include<stdlib.h>
#include<ctime>
#include<sys/types.h>
#include<unistd.h>
#include"blockqueue.hpp"
using namespace std;void* producter(void* args)
{blockqueue<int> * _pbq=static_cast<blockqueue<int>*>(args);int num=0;while(true){num=rand()%200+1;//设置随机数据_pbq->push(num);//生产者放入数据cout<<"producter push num: "<<num<<" in bq"<<endl;sleep(1);}return nullptr;
}void* consumer(void* args)
{blockqueue<int> * _cbq=static_cast<blockqueue<int>*>(args);int ret=0;while(true){ _cbq->pop(&ret);//消费者取出数据cout<<"consumer take num: "<<ret<<" from bq"<<endl;// sleep(1);}return nullptr;
}int main()
{srand((unsigned long)time(nullptr)^getpid());blockqueue<int>* _bq= new blockqueue<int>();
pthread_t p,c;//int n=pthread_create(&p,nullptr,producter,(void*)_bq);//生产者线程assert(n==0); int m=pthread_create(&p,nullptr,consumer,(void*)_bq);//消费者线程assert(m==0);pthread_join(p,nullptr);//回收生产者线程pthread_join(c,nullptr);//回收消费者线程delete _bq;//删除队列return 0;
}

blockqueue.hpp(以.hpp开头的文件可以将定义和实现放一起,调用者只需要调用该文件即可)

#include<iostream>
#include<pthread.h>
#include<assert.h>
#include<queue>
using namespace std;
#define GMAXCAP 5//宏定义阻塞队列的最大容量template<class T>
class blockqueue
{public:blockqueue()//构造:_maxcap(GMAXCAP){pthread_mutex_init(&_mut,nullptr);//初始化互斥锁pthread_cond_init(&_pcond,nullptr);//初始化生产者的条件变量pthread_cond_init(&_ccond,nullptr);//初始化消费者的条件变量}void push(T&in)//输入型参数用&{pthread_mutex_lock(&_mut);//加锁while(is_full()){pthread_cond_wait(&_pcond,&_mut);//若队列为满,生产者需要阻塞等待,直到队列不为满才能放数据}
//走到这就放数据
_q.push(in);//往队列中放数据
pthread_mutex_unlock(&_mut);//解锁
pthread_cond_signal(&_ccond);//唤醒消费者线程}void pop(T* out)//输出型参数用*。输入输出型参数用&{
pthread_mutex_lock(&_mut);//加锁
while(is_empty())//队列为空消费者就需要阻塞等待,直到队列中至少存在一个数据{pthread_cond_wait(&_ccond,&_mut);}
//走到这可以取数据
*out=_q.front();
_q.pop();
pthread_mutex_unlock(&_mut);//解锁
pthread_cond_signal(&_pcond);//唤醒生产者线程}~blockqueue()
{
pthread_mutex_destroy(&_mut);//释放互斥锁
pthread_cond_destroy(&_pcond);//释放生产者条件变量
pthread_cond_destroy(&_ccond);//释放消费者生产变量
}
bool is_full()
{return _q.size()==_maxcap;//判断队列是否为满
}
bool is_empty()
{return _q.size()==0;//判断队列是否为空
}private:
queue<T> _q;//队列
int _maxcap;//队列中的最大容量
pthread_mutex_t _mut;//互斥锁
pthread_cond_t _pcond;//生产者的条件变量
pthread_cond_t _ccond;//消费者的条件变量
};
  • 阻塞队列要给生产者往里push数据,让消费者从中pop数据,那么该阻塞队列需要被这两个线程所看到。因此在创建生产者线程和消费者线程时,需要将阻塞队列作为参数传参给者两个线程
  • 生产者负责生产随机数并往阻塞队列中push,push成功并打印日志;消费者负责从阻塞队列中拿取数据,拿取成功并打印日志
  • 我们实现的是单生产者单消费者模型,需要维护生产者和消费者之间的互斥和同步关系
  • blockqueue队列存储数据的上限为5,当队列中存储了5组数据后生产者将会阻塞不能生产
  • 生产者线程在判断is_full的时候用的是while而if,理由如下:(消费者线程相同)
  • 无论是生产者线程还是消费者线程都是先加锁再进行判断是否满足条件。以生产者线程来说,若判断is_full是用的if,那么有可能函数pthread_wait调用失败,那么继续往后走就会出问题;其次是在多生产者的情况下,是可能唤醒生产者线程的函数是pthread_cond_broadcast,那么情况是唤醒全部的生产者线程,其实待唤醒的线程就一个,就会导致伪唤醒而造成多个线程进入临界区的情况。为了避免以上情况,我们又需要先加锁再判断满足条件,就需要用到while来判断
  • 当生产者push一个数据后就意味着阻塞队列中至少有一个数据,若此时消费者线程在is_empty里面阻塞等待的话,生产者线程就会唤醒消费者线程醒来;相同的当消费者线程pop一个数据后意味着阻塞队列中至少有一个空间,消费者线程就会唤醒生产者线程醒来。
  • 生产者生产地慢,消费者消费地块

image-20230720190241978

  • 生产者每生产一个数据,往队列中push,消费者就从队列里拿一个数据并pop,呈现出生产者线程和消费者线程交错执行
  • 生产者生产地块,消费者消费地慢

image-20230720191433899

  • 可以看到先是生产者生产了五个数据并push进阻塞队列,此时阻塞队列满了生产者生产失败需要消费者去pop数据,然后消费者消费一个数据,队列中有了空位,生产者生产一个数据并push,然后呈现出这两个线程交替执行

基于计算任务和存储任务的生产者消费者模型

image-20230721193428752

  • 根据图可以看到,我们维护的还是单线程阻塞队列,比之前的模型多了一个阻塞队列和线程

blockqueue.hpp

#pragma once
#include<iostream>
#include<pthread.h>
#include<assert.h>
#include<queue>
using namespace std;#define GMAXCAP 5//宏定义阻塞队列的最大容量
template<class T>
class blockqueue
{public:blockqueue()//构造:_maxcap(GMAXCAP){pthread_mutex_init(&_mut,nullptr);//初始化互斥锁pthread_cond_init(&_pcond,nullptr);//初始化生产者的条件变量pthread_cond_init(&_ccond,nullptr);//初始化消费者的条件变量}void push(T&in)//输入型参数用&{pthread_mutex_lock(&_mut);//加锁while(is_full()){pthread_cond_wait(&_pcond,&_mut);//若队列为满,生产者需要阻塞等待,直到队列不为满才能放数据}
//走到这就放数据
_q.push(in);//往队列中放数据
pthread_mutex_unlock(&_mut);//解锁
pthread_cond_signal(&_ccond);//唤醒消费者线程}void pop(T* out)//输出型参数用*。输入输出型参数用&
{
pthread_mutex_lock(&_mut);//加锁
while(is_empty())//队列为空消费者就需要阻塞等待,直到队列中至少存在一个数据
{pthread_cond_wait(&_ccond,&_mut);
}
//走到这可以取数据
*out=_q.front();
_q.pop();
pthread_mutex_unlock(&_mut);//解锁
pthread_cond_signal(&_pcond);//唤醒生产者线程
}~blockqueue()
{
pthread_mutex_destroy(&_mut);//释放互斥锁
pthread_cond_destroy(&_pcond);//释放生产者条件变量
pthread_cond_destroy(&_ccond);//释放消费者生产变量
}
bool is_full()
{return _q.size()==_maxcap;//判断队列是否为满
}bool is_empty()
{return _q.size()==0;//判断队列是否为空
}private:
queue<T> _q;//队列
int _maxcap;//队列中的最大容量
pthread_mutex_t _mut;//互斥锁
pthread_cond_t _pcond;//生产者的条件变量
pthread_cond_t _ccond;//消费者的条件变量
};
  • 可以看到这里的阻塞队列的.hpp文件和之前的单生产者单消费者模型的文件是一样的

main.cc

#include<iostream>
#include<pthread.h>
#include<assert.h>
#include<queue>
#include<stdlib.h>
#include<ctime>
#include<sys/types.h>
#include<unistd.h>
#include"blockqueue.hpp"
#include"task.hpp"
using namespace std;int mymath(int x,int y,char op)//计算任务需要调用的函数即计算四则运算并返回结果
{int ret=0;switch(op){case '+':ret=x+y;break;case '-':ret=x-y;break;  case '*':ret=x*y;break;        case '/':if(y==0){cerr<<"div zero erro!"<<endl;ret=-1;}else{ret=x/y;}break;        case '%':if(y==0){cerr<<"div zero erro!"<<endl;ret=-1;}else{ret=x%y;}break;default://do nothingbreak;}return ret;
}template<class C,class S>//C-calculate,S-save
class blockqueues
{public:
blockqueue<C>* c_bq;//计算队列
blockqueue<S>* s_bq;//存储队列
};const string Goper="+-*/%";//运算符集合void* producter(void* args)//生产者
{blockqueue<Caltask> * _cbq=(static_cast<blockqueues<Caltask,SaveTask>*>(args))->c_bq;while(true){int x=rand()%200+1;//设置随机数据int y=rand()%100;//设置随机数据int num=rand()%Goper.size();//随机运算符Caltask cal(x,y,Goper[num],mymath);//创建计算任务对象_cbq->push(cal);//生产者放入数据cout<<"producter push num: "<<cal.taskstringforP()<<" in bq"<<endl;sleep(1);}return nullptr;
}void* consumer(void* args)//消费者
{blockqueue<Caltask> * _cbq=(static_cast<blockqueues<Caltask,SaveTask>*>(args))->c_bq;//取出传过来的参数中的c_bq队列blockqueue<SaveTask>* _sbq=(static_cast<blockqueues<Caltask,SaveTask>*>(args))->s_bq;//取出传过来的参数中的s_bq队列while(true){Caltask ret;_cbq->pop(&ret);//消费者取出数据cout<<"consumer take savetask: "<<ret()<<" from bq"<<endl;string result=ret();SaveTask sat(result,tosave);_sbq->push(sat);cout<<"consumer push savetask to bq"<<endl;// sleep(1);}return nullptr;
}void* saver(void* args)
{
blockqueue<SaveTask>* _sbq=(static_cast<blockqueues<Caltask,SaveTask>*>(args))->s_bq;
while(true)
{
SaveTask ret;
_sbq->pop(&ret);//saver取出数据
ret();//调用仿函数
cout<<"saver finish task"<<endl;
}
return nullptr;
}int main()
{srand((unsigned long)time(nullptr)^getpid());
blockqueues<Caltask,SaveTask> _bqs;
_bqs.c_bq=new blockqueue<Caltask>();//创建计算队列
_bqs.s_bq=new blockqueue<SaveTask>();//创建存储队列pthread_t p,c,s;int n=pthread_create(&p,nullptr,producter,&_bqs);//生产者线程assert(n==0); int m=pthread_create(&c,nullptr,consumer,&_bqs);//消费者线程assert(m==0);int k=pthread_create(&s,nullptr,saver,&_bqs);//存储线程assert(k==0);pthread_join(p,nullptr);//回收生产者线程pthread_join(c,nullptr);//回收消费者线程pthread_join(s,nullptr);//回收消费者线程delete _bqs.c_bq;//删除队列delete _bqs.s_bq;//删除队列return 0;
}
  • 创建一个blockqueues对象,里面有两个成员分别是计算任务对应的队列c_bq和存储任务对应的队列s_bq
  • delete对象时,不能直接删除blockqueues对象,这样会造成内存泄漏,需要删除里面的成员即队列
  • 生产者负责生产随机参数一、随机参数二、随机运算符将这些参数传递给Caltask对象,然后将Caltask放入到阻塞队列c_bq中
  • 消费者负责从队列中取出Caltask对象并调用对象的仿函数进行运算并返回运算式字符串加上打印运算式,然后将运算式字符串放入s_bq中,并且打印日志
  • saver负责将运算式字符串从s_bq中取出,然后将运算式字符串存储到当前路径的log.txt文件中,并且打印日志
  • 还需注意的是,这个模型的节奏是按照生产者来的,生产者隔一秒生产一个计算任务,然后消费者消费一次,saver存储一次,即生产者生产的慢,消费者消费的快从而saver存储的快

task.hpp

#pragma once
#include<stdlib.h>
#include<functional>
using namespace std;
class Caltask
{typedef function<int(int,int,char)> fun_c;
public:
Caltask(){}//无参构造Caltask(int x,int y,char op,fun_c func)
:_x(x)
,_y(y)
,_op(op)
,_caltask(func)
{}string operator()()//()运算符重载
{
int ret=_caltask(_x,_y,_op);//调用外部传进来的计算任务
char buffer[128];
snprintf(buffer,sizeof buffer,"%d %c %d =%d",_x,_op,_y,ret);
return buffer;
}string taskstringforP()//供外部调用打印运算式字符串
{char buffer[128];snprintf(buffer,sizeof buffer,"%d %c %d =?",_x,_op,_y);return buffer;
}private:
int _x;//参数一
int _y;//参数二
char _op;//运算符号
fun_c _caltask;//需调用的外部计算函数
};class SaveTask
{typedef function<void(string)> fun_c;
public:
SaveTask(){}//默认构造SaveTask(  string & s,fun_c func)
:_str(s)
,_func(func)
{}void operator()()
{_func(_str);
}private:
string _str;
fun_c _func;
};void tosave(const string&s)
{string target="./log.txt";//文件的路径FILE*fp=fopen(target.c_str(),"a+");// 以追加的方式打开文件if(!fp)//文件打开失败{cout<<"fopen error"<<endl;}fputs(s.c_str(),fp);//往文件里面写数据fputs("\n",fp);// 往文件里面写换行符fclose(fp);//关闭文件
}

image-20230721193335124

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

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

相关文章

代码随想录| 图论02●695岛屿最大面积 ●1020飞地的数量 ●130被围绕的区域 ●417太平洋大西洋水流问题

#695岛屿最大面积 模板题&#xff0c;很快.以下两种dfs&#xff0c;区别是看第一个点放不放到dfs函数中处理&#xff0c;那么初始化的area一个是1一个是0 int dir[4][2]{0,1,0,-1,1,0,-1,0};void dfs(int x, int y,int n, int m, int &area,vector<vector<bool>…

2023最新谷粒商城笔记之Sentinel概述篇(全文总共13万字,超详细)

Sentinel概述 服务流控、熔断和降级 什么是熔断 当扇出链路的某个微服务不可用或者响应时间太长时&#xff0c;会进行服务的降级&#xff0c;**进而熔断该节点微服务的调用&#xff0c;快速返回错误的响应信息。**检测到该节点微服务调用响应正常后恢复调用链路。A服务调用B服…

构建高效供应商管理体系,提升企业采购能力

随着企业采购规模的不断扩大和全球化竞争的加剧&#xff0c;供应商管理变得越来越重要。构建一个高效的供应商管理体系是企业提升采购能力、降低采购成本的关键一环。本文将重点探讨供应商管理体系的意义和作用&#xff0c;并介绍如何构建一个高效的供应商管理体系。 一、供应商…

SpringBoot复习:(1)常用的SpringApplication.run返回的容器的具体类型是哪个?

run方法中调用了createApplicationContext方法 createApplicationContext方法代码如下&#xff1a; 其中create代码如下&#xff1a; 可见返回的是AnnotationConfigServletWebServerApplicationContext()

【搜索引擎Solr】配置 Solr 以获得最佳性能

Apache Solr 是广泛使用的搜索引擎。有几个著名的平台使用 Solr&#xff1b;Netflix 和 Instagram 是其中的一些名称。我们在 tajawal 的应用程序中一直使用 Solr 和 ElasticSearch。在这篇文章中&#xff0c;我将为您提供一些关于如何编写优化的 Schema 文件的技巧。我们不会讨…

基于Python+WaveNet+CTC+Tensorflow智能语音识别与方言分类—深度学习算法应用(含全部工程源码)

目录 前言总体设计系统整体结构图系统流程图 运行环境Python 环境Tensorflow 环境 模块实现1. 方言分类数据下载及预处理模型构建模型训练及保存 2. 语音识别数据预处理模型构建模型训练及保存 3. 模型测试功能选择界面语言识别功能实现界面方言分类功能实现界面 系统测试1. 训…

【RabbitMQ(day1)】RabbitMQ的概述和安装

入门RabbitMQ 一、RabbitMQ的概述二、RabbitMQ的安装三、RabbitMQ管理命令行四、RabbitMQ的GUI界面 一、RabbitMQ的概述 MQ&#xff08;Message Queue&#xff09;翻译为消息队列&#xff0c;通过典型的【生产者】和【消费者】模型&#xff0c;生产者不断向消息队列中生产消息&…

【DDD】业务领域定义

文章目录 前言一、什么是业务子领域&#xff1f;二、子领域的类型有哪些&#xff1f;2.1、核心子领域2.2、通用子领域2.3、支撑子领域 三、子领域差异对比3.1、竞争优势比较3.2、复杂性比较3.3、易变性比较3.4、实时策略比较 总结 前言 一个业务领域是一个公司的主要活动领域的…

redis(11):springboot中使用redis

1 创建springboot项目 2 创建pom文件 <?xml version"1.0" encoding"UTF-8"?> <project xmlns"http://maven.apache.org/POM/4.0.0" xmlns:xsi"http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation"http:/…

vue3+Luckysheet实现表格的在线预览编辑(electron可用)

前言&#xff1a; 整理中 官方资料&#xff1a; 1、github 项目地址https://github.com/oy-paddy/luckysheet-vue-importAndExport/tree/master/https://github.com/oy-paddy/luckysheet-vue-importAndExport/tree/master/ 2、xlsx vue3 json数据导出excel_vue3导出excel_羊…

【SpirngCloud】分布式事务解决方案

【SpirngCloud】分布式事务解决方案 文章目录 【SpirngCloud】分布式事务解决方案1. 理论基础1.1 CAP 理论1.2 BASE 理论1.3 分布式事务模型 2. Seata 架构2.1 项目引入 Seata 3. 强一致性分布式事务解决方案3.1 XA 模式3.1.1 seata的XA模式3.1.2 XA 模式实践3.1.3 总结 4. 最终…

React AntDesign表批量操作时的selectedRowKeys回显选中

不知道大家是不是在AntDesign的某一个列表想要做一个批量导出或者操作的时候&#xff0c;发现只要选择下一页&#xff0c;即使选中的ids 都有记录下面&#xff0c;但是就是不回显 后来问了chatGPT&#xff0c;对方的回答是&#xff1a; 在Ant Design的DataTable组件中&#xf…

什么是框架?为什么要学框架?

一、什么是框架 框架是整个或部分应用的可重用设计&#xff0c;是可定制化的应用骨架。它可以帮开发人员简化开发过程&#xff0c;提高开发效率。 项目里有一部分代码&#xff1a;和业务无关&#xff0c;而又不得不写的代码>框架 项目里剩下的部分代码&#xff1a;实现业务…

基于C++的QT基础教程学习笔记

文章目录&#xff1a; 来源 教程社区 一&#xff1a;QT下载安装 二&#xff1a;注意事项 1.在哪里写程序 2.如何看手册 3.技巧 三&#xff1a;常用函数 1.窗口 2.相关 3.按钮 4.信号与槽函数 5.常用栏 菜单栏 工具栏 状态栏 6.铆接部件 7.文本编辑 8…

Docker Compose(九)

一、背景&#xff1a; 对于现代应用来说&#xff0c;大多数都是通过很多的微服务互相协同组成一个完整的应用。例如&#xff0c;订单管理、用户管理、品类管理、缓存服务、数据库服务等&#xff0c;他们构成了一个电商平台的应用。而部署和管理大量的服务容器是一件非常繁琐的事…

【时间复杂度】

旋转数组 题目 给定一个整数数组 nums&#xff0c;将数组中的元素向右轮转 k 个位置&#xff0c;其中 k 是非负数。 /* 解题思路&#xff1a;使用三次逆转法&#xff0c;让数组旋转k次 1. 先整体逆转 // 1,2,3,4,5,6,7 // 7 6 5 4 3 2 1 2. 逆转子数组[0, k - 1] // 5 6 7 4 3…

疲劳驾驶检测和识别2:Pytorch实现疲劳驾驶检测和识别(含疲劳驾驶数据集和训练代码)

疲劳驾驶检测和识别2&#xff1a;Pytorch实现疲劳驾驶检测和识别(含疲劳驾驶数据集和训练代码) 目录 疲劳驾驶检测和识别2&#xff1a;Pytorch实现疲劳驾驶检测和识别(含疲劳驾驶数据集和训练代码) 1.疲劳驾驶检测和识别方法 2.疲劳驾驶数据集 &#xff08;1&#xff09;疲…

MySQL 8.0 OCP (1Z0-908) 考点精析-性能优化考点6:MySQL Enterprise Monitor之Query Analyzer

文章目录 MySQL 8.0 OCP (1Z0-908) 考点精析-性能优化考点6&#xff1a;MySQL Enterprise Monitor之Query AnalyzerMySQL Enterprise Monitor之Query AnalyzerQuery Response Time index (QRTi)例题例题1: Query Analyzer答案与解析1 参考 【免责声明】文章仅供学习交流&#x…

vue中如何通过webpack-bundle-analyzer打包分析工具进行配置优化

vue中随着项目的不断功能迭代和开发&#xff0c;项目文件越来越多&#xff0c;项目的打包文件也越来越大。如何对打包文件进行分析优化&#xff0c;减小打包文件大小呢&#xff1f;可以通过webpack-bundle-analyzer 这个打包分析工具进行解决。 1、webpack-bundle-analyzer的安…

Python Flask构建微信小程序订餐系统 (十一)

🔥 已经删除的会员不允许进行编辑昵称 🔥 🔥 已经删除的会员要隐藏掉会员信息的编辑按钮 🔥 🔥 创建商品表 food 🔥 CREATE TABLE `food` (`id` int(11) unsigned NOT NULL AUTO_INCREMENT,`cat_id` int(11) NOT NULL DEFAULT 0 COMMENT 分类id,`name` varchar…