生产者消费者模型

生产者消费者模型

位图 (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 nothing
        break;
    }
    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.mfbz.cn/a/42773.html

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

相关文章

C++ 单例模式(介绍+实现)

文章目录 一. 设计模式二. 单例模式三. 饿汉模式四. 懒汉模式结束语 一. 设计模式 单例模式是一种设计模式 设计模式(Design Pattern)是一套被反复使用&#xff0c;多数人知晓的&#xff0c;经过分类的&#xff0c;代码设计经验的总结。 为什么要有设计模式 就像人类历史发展会…

python机器学习(三)特征预处理、鸢尾花案例--分类、线性回归、代价函数、梯度下降法、使用numpy、sklearn实现一元线性回归

K-近邻算法(K-Nearest Neighboor) 特征预处理 数据预处理的过程。数据存在不同的量纲、数据中存在离群值&#xff0c;需要稳定的转换数据&#xff0c;处理好的数据才能更好的去训练模型&#xff0c;减少误差的出现。 标准化 数据集的标准化对scikit-learn中实现的大多数机器…

遥感目标检测(3)-DAL(Dynamic Anchor Learning for Object Detection)

目录 一、概述 二、背景 三、建议 1、旋转RetinaNet 2、动态锚框分布 3、匹配敏感损失 四、实验 一、概述 由于选择正样本锚框进行回归&#xff0c;不一定能够定位真实的GT&#xff0c;而部分负样本回归甚至可以回归到真实的GT&#xff0c;说明相当多的负样本锚框有着准…

【自启动配置】Ubuntu 设置开机自启动脚本

Ubuntu 开机运行的脚本和当前操作系统运行的级别有关&#xff0c;OS 的运行级别大概分为七个 目录 1、查看 OS 运行级别 2、创建自启动脚本 3、添加软链接 1、查看 OS 运行级别 输入命令 runlevel 查看当前系统运行级别。当前系统的运行级别为 5 2、创建自启动脚本 在 /et…

ZooKeeper原理剖析

1.ZooKeeper简介 ZooKeeper是一个分布式、高可用性的协调服务。在大数据产品中主要提供两个功能&#xff1a; 帮助系统避免单点故障&#xff0c;建立可靠的应用程序。提供分布式协作服务和维护配置信息。 2.ZooKeeper结构 ZooKeeper集群中的节点分为三种角色&#xff1a;Le…

多线程(JavaEE初阶系列2)

目录 前言&#xff1a; 1.什么是线程 2.为什么要有线程 3.进程与线程的区别与联系 4.Java的线程和操作系统线程的关系 5.多线程编程示例 6.创建线程 6.1继承Thread类 6.2实现Runnable接口 6.3继承Thread&#xff0c;使用匿名内部类 6.4实现Runnable接口&#xff0c;使…

html2Canvas+JsPDF 导出pdf 无法显示网络图片

html2CanvasJsPDF 导出pdf 问题&#xff1a;类似于下面着这种网络图片使用img导出的时候是空白的 https://gimg3.baidu.com/search/srchttp%3A%2F%2Fpics4.baidu.com%2Ffeed%2F7e3e6709c93d70cf827fb2fda054500cb8a12bc9.jpeg%40f_auto%3Ftoken%3Dd97d3f0fd06e680e592584f8c7a2…

深度学习——LSTM解决分类问题

RNN基本介绍 概述 循环神经网络&#xff08;Recurrent Neural Network&#xff0c;RNN&#xff09;是一种深度学习模型&#xff0c;主要用于处理序列数据&#xff0c;如文本、语音、时间序列等具有时序关系的数据。 核心思想 RNN的关键思想是引入了循环结构&#xff0c;允许…

分布式 - 消息队列Kafka:Kafka分区常见问题总结

文章目录 01. Kafka 的分区是什么&#xff1f;02. Kafka 为什么需要分区&#xff1f;03. Kafka 分区有什么作用&#xff1f;03. Kafka 为什么使用分区的概念而不是直接使用多个主题呢&#xff1f;04. Kafka 分区的数量有什么限制&#xff1f;05. Kafka 分区的副本有什么作用&am…

动态内存管理基础详解

目录 1、为什么存在动态内存分配 2、动态内存函数的介绍 2.1 malloc和free 功能&#xff1a; 参数和返回值&#xff1a; 注意事项&#xff1a; tip: 2.2 calloc 2.3 realloc函数 功能&#xff1a; 参数和返回值&#xff1a; realloc开辟空间的两种情况 realloc会顺…

Rust操作MySQL

查询 本部分是对 「Rust入门系列」Rust 中使用 MySQL[1]的学习与记录 经常使用的时间处理库&#xff1a; chrono 流式查询使用&#xff1a; query_iter 输出到Vec使用&#xff1a; query 映射到结构体使用&#xff1a; query_map 获取单条数据使用&#xff1a; query_first 命名…

Hadoop简介以及集群搭建详细过程

Hadoop简介以及集群搭建详细过程 hadoop集群简介hadoop部署模式Hadoop集群安装1.集群角色规划2.服务器基础环境准备3.上传安装包hadoop安装包目录结构5.编辑hadoop配置文件6.分发安装包7.配置hadoop环境变量8.NameNode format(格式化操作) hadoop集群启动关闭-手动逐个进程启停…

漏洞复现-yapi远程执行命令漏洞复现

目录 漏洞原理漏洞发现漏洞描述影响范围 yapi学习漏洞复现环境搭建exp 入侵检测与防御参考 漏洞原理 漏洞发现 查看issue2229 漏洞描述 网站开放注册功能时可随意注册&#xff0c;设置全局mock脚本可执行任意代码。 影响范围 Yapi < 1.9.2 yapi学习 YApi 是高效、易…

vue3前端分页,全选翻页状态保持

直接贴代码&#xff0c;代码中有注释 <template><div class"viewer-container" id"viewer-container"><!-- 表格 --><el-table:row-key"getRowKeys":data"data.tableDataCopy"style"width: 100%"ref&…

Spring详解(学习总结)

目录 一、Spring概述 &#xff08;一&#xff09;、Spring是什么&#xff1f; &#xff08;二&#xff09;、Spring框架发展历程 &#xff08;三&#xff09;、Spring框架的优势 &#xff08;四&#xff09;、Spring的体系结构 二、程序耦合与解耦合 &#xff08;一&…

消息队列——rabbitmq的不同工作模式

目录 Work queues 工作队列模式 Pub/Sub 订阅模式 Routing路由模式 Topics通配符模式 工作模式总结 Work queues 工作队列模式 C1和C2属于竞争关系&#xff0c;一个消息只有一个消费者可以取到。 代码部分只需要用两个消费者进程监听同一个队里即可。 两个消费者呈现竞争关…

【itext7】itext7操作PDF文档之添加段落文本内容、添加List列表、添加Image图片、添加Table表格

这篇文章&#xff0c;主要介绍itext7操作PDF文档之添加段落文本内容、添加List列表、添加Image图片、添加Table表格。 目录 一、itext7操作PDF内容 1.1、添加段落文本内容 1.2、添加列表内容 1.3、添加图片 1.4、添加表格 &#xff08;1&#xff09;列宽采用点单位&#…

情绪即需求

情绪即需求 心理学认为&#xff0c;每个情绪背后都藏着一个未被满足的心里需求. 模型介绍 每一个情绪背后&#xff0c;都有一个未被满足的心理需求。情绪没有好坏之分&#xff0c;存在即合理。情绪是人类不断进化的产物&#xff0c;每一种情绪都是在保护我们&#xff0c;都有其…

基于Java+SpringBoot+vue前后端分离校园周边美食探索分享平台设计实现

博主介绍&#xff1a;✌全网粉丝30W,csdn特邀作者、博客专家、CSDN新星计划导师、Java领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精彩专…

OpenCv之特征检测

目录 一、基本概念 二、harris角点检测 三、SIFT算法 四、Shi-Tomasi角点检测 一、基本概念 特征检测指的是使用计算机提取图像信息&#xff0c;决定每个图像的点是否属于一个图像特征。特征检测的结果是把图像上的点分为不同的子集&#xff0c;这些子集往往属于孤立的点、…
最新文章