015 Linux_生产消费模型

​🌈个人主页:Fan_558
🔥 系列专栏:Linux
🌹关注我💪🏻带你学更多操作系统知识
在这里插入图片描述

文章目录

  • 前言
    • 一、生产消费模型
      • (1)概念引入
      • (2)生产消费模型的优点
      • (3)生产消费模型的特点
    • 二、基于阻塞队列的生产消费模型
    • 三、基于环形队列的生产消费模型
      • (1)环形队列的生产消费模型特点
  • 小结

前言

本文将会向你介绍基于阻塞队列和环形队列的生产消费模型

一、生产消费模型

(1)概念引入

1、什么是生产者消费者模型?
生产者和消费是操作系统中一种重要的模型,它描述的是一种等待和通知的机制
2、本文定义:

生产者: 产生数据的模块,就形象地称为生产者;
消费者: 而处理数据的模块,就称为消费者;
缓冲区: 生产者和消费者之间的中介就叫做缓冲区。

什么是阻塞队列

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

在这里插入图片描述

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

(2)生产消费模型的优点

优点1、解决了强耦合问题
假设生产者和消费者分别是两个类。如果让生产者直接调用消费者的某个方法,那么生产者对于消费者就会产生依赖(也就是耦合)。将来如果消费者的代码发生变化,可能会影响到生产者。而如果两者都依赖于某个缓冲区,两者之间不直接依赖,耦合也就相应降低了。
就好比说,你需要买零食,你还要给生产厂家打电话,然后他如果没有的话,还要制作,然后在交给你,这多麻烦呀,万一厂家哪天电话一变(相当于生产者线程的代码变化导致消费者的代码也要变化)那么你还得问问电话是多少。

优点2、支持并发(concurrency)即生产者和消费者可以是两个独立的并发主体,互不干扰的运行。
生产者直接调用消费者的某个方法,还有另一个弊端。由于函数调用是同步的(或者叫阻塞的),在消费者的方法没有返回之前,生产者只好一直等在那边。万一消费者处理数据很慢,生产者就会白白糟蹋大好时光。
使用了生产者/消费者模式之后,生产者和消费者可以是两个独立的并发主体(常见并发类型有进程和线程两种)。生产者把制造出来的数据往缓冲区一丢,就可以再去生产下一个数据。基本上不用依赖消费者的处理速度。

优点3、支持忙闲不均
如果制造数据的速度时快,时慢,缓冲区可以对其进行适当缓冲。当数据制造快的时候,消费者来不及处理,未处理的数据可以暂时存在缓冲区中。等生产者的制造速度慢下来,消费者再慢慢处理掉。

(3)生产消费模型的特点

  • 首先,生产者只需要关心“仓库”,并不需要关心具体的消费者。
  • 对于消费者而言,它不需要关心具体的生产者,它只需要关心这个“仓库”中还有没有东西存在。
  • 生产者生产的时候消费者不能进行“消费”,消费者消费的时候生产者不能生产,相当于一种互斥关系,
    即生产者和消费者一次只能有一人能访问到“仓库”。
  • “仓库”为空时不能进行消费。
  • “仓库”满时不能进行生产。
    在这里插入图片描述
    综上所述:可以记忆三二一原则:
    即三种关系、两个角色、一个场所
    1、三种关系:
  • 生产者与生产者(互斥)
    举个例子:生产商作为生产者的身份,生产者需要获得利益的最大化,当然是希望自己一家独大,那么生产者和生产者之间就是互斥的,最起码在1号生产商正在生产时,2号不能来捣乱,体现在代码中就是互斥
  • 生产者与消费者(同步与互斥)
    互斥:举个例子:比如当前你正要使用一号间厕所,此时清洁人员来了,那么他就需要在外面等待,同理他正在清理一号间,你也不能进入一号间
    同步:举个例子:如果你当前正在使用一号间厕所,那么清洁人员是可以清洁二号间的
  • 消费者与消费者(互斥)
    举个例子:两个人不能同时使用一间厕所

2、两个角色:

  • 生产者
  • 消费者

3、一个场所:

  • 缓冲区

基于以上特点,我们来编写代码吧

二、基于阻塞队列的生产消费模型

//Task.hpp

#pragma once
#include <iostream>
#include <string>
std::string oper_="+-*/%";
enum{
    DivZero = 1,
    ModZero = 2,
    UnKnown
};

class Task
{
public:
    Task(int x, int y, char op)
    :data1_(x)
    ,data2_(y)
    ,oper_(op)
    ,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;
    }
}
void operator()()
{
    run();
}
//获取结果信息
std::string GetResult()
{
    std::string r = std::to_string(data1_);
    r += ' ';
    r += oper_;
    r += ' ';
    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 += ' ';
    r += oper_;
    r += ' ';
    r += std::to_string(data2_);
    r += " =?";
    return r;
}
private:
    int data1_;
    int data2_;
    char oper_;
    int result_;
    int exitcode_;
};
//BlockQueue.hpp

#include "Task.hpp"
#include <iostream>
#include <queue>
#include <pthread.h>
#include <unistd.h>
template <class T>
class BlockQueue
{
    //初始值
    static const int defalutnum = 10;
public:
    BlockQueue(int maxcap = defalutnum)
    :maxcap_(maxcap)
    {
        pthread_mutex_init(&mutex_, nullptr);
        pthread_cond_init(&c_cond_, nullptr);
        pthread_cond_init(&p_cond_, nullptr);
        //定义上下限,满足下限就唤醒生产者生产,满足上限就唤醒消费者消费
        low_water_  = maxcap_ / 3;
        high_water_ = (maxcap_*2) / 3;
    }
    //消费
    const T &pop()
    {
        pthread_mutex_lock(&mutex_);
        //条件变量
        while(q_.size() == 0)
        {
            //进入等待队列中排队
            pthread_cond_wait(&c_cond_, &mutex_);
        }
        T out = q_.front();
        q_.pop();
        if(q_.size() < low_water_)
        {
            pthread_cond_signal(&p_cond_);
        }
        //唤醒消费者
        pthread_mutex_unlock(&mutex_);
        return out;
    }
    //生产
    void push(const T &in)
    {
        pthread_mutex_lock(&mutex_);
        //条件变量
        while(q_.size() == maxcap_)
        {
            //进入等待队列中排队
            pthread_cond_wait(&p_cond_, &mutex_);
        }
        //生产一个数据
        q_.push(in);
        //唤醒消费者
        if(q_.size() > high_water_)
        {
            pthread_cond_signal(&c_cond_);
        }
 
        pthread_mutex_unlock(&mutex_);
    }
    //析构
    ~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 p_cond_;   //生产者条件变量
    pthread_cond_t c_cond_;   //消费者条件变量
    int low_water_;
    int high_water_;
};

值得一提的是条件变量的判断部分我们不能使用if,而是要用while,原因是防止产生伪唤醒的现象,消费者只消费了一个,但是唤醒时,(如果我们使用broadcast去唤醒生产者线程),它们就不会在条件变量下等待了,而是都去竞争这个锁了。如果此时一个生产者线程竞争成功后就去生产(假设只剩下一个空位供生产),然后它生产后,紧接着唤醒消费者线程去消费,然后解锁,但是此时还有另外两个生产者线程在竞争锁,他们都在条件变量下等待,然后有可能又是生产者线程拿到锁(消费者却没有竞争到锁),继续向后执行,但是呢,if条件不会再判断了,函数返回继续向下执行,结果就是生产多了,已经没有空间去生产了,这就是伪唤醒状态

总而言之,while循环会让我们对(上述背景中等待的)生产者们再次判断是否满足生产的条件,防止产生伪唤醒的现象
在这里插入图片描述

//main.cc

#include "BlockQueue.hpp"
#include "Task.hpp"
#include <stdlib.h>
//生产
void* Productor(void* args)
{
    BlockQueue<Task> *bq = static_cast<BlockQueue<Task> *>(args);
    //sleep(5);
    while(true)
    {
        int number1 = rand() % 10 + 1;
        usleep(10);
        //让随机数number2出现0的情况
        int number2 = rand() % 10;
        int r = rand() % 5;
        Task t(number1, number2, oper_[r]);
        bq->push(t);     //传入任务信息
        std::cout << "thread id: "<< pthread_self() <<" "<< "传入任务:" << t.GetTask() <<std::endl;
        sleep(1);
    }
}
//消费
void* Consumer(void* args)
{
    BlockQueue<Task> *bq = static_cast<BlockQueue<Task> *>(args);
    while(true)
    {
        Task ret = bq->pop();
        ret();
        std::cout << "thread id: "<< pthread_self() <<" "<< "run...:" << ret.GetResult() <<std::endl;
        sleep(1);
    }
}

int main()
{
    //创建指向阻塞队列的指针(其中队列中放着一个个Task对象(任务))
    BlockQueue<Task> *bq = new BlockQueue<Task>();

    pthread_t c[3], p[5];
    //创建生产、消费线程
    for(int i = 0; i < 5; i++)
    {
        pthread_create(p + i, nullptr, Productor, bq);
    }
    for(int i = 0; i < 3; i++)
    {
        pthread_create(c + i, nullptr, Consumer, bq);
    }

    //线程等待
    for(int i = 0; i < 5; i++)
    {
        pthread_join(p[i], nullptr);
    }
        for(int i = 0; i < 3; i++)
    {
        pthread_join(c[i], nullptr);
    }
    delete bq;

    return 0;
}

运行结果
在这里插入图片描述

三、基于环形队列的生产消费模型

(1)环形队列的生产消费模型特点

环形队列采用数组模拟,用模运算来模拟环状特性。
环形结构起始状态和结束状态都是一样的,不好判断为空或者为满,所以可以通过加计数器或者标记位来判断满或者空。另外也可以预留一个空的位置,作为满的状态。但是我们现在有信号量这个计数器,就很简单的进行多线程间的同步过程
在这里插入图片描述

初始化信号量
#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value);
参数:
  pshared:0表示线程间共享,非零表示进程间共享
  value:信号量初始值

销毁信号量
int sem_destroy(sem_t *sem);

等待信号量
功能:等待信号量,使信号量-1,若信号量为负数,那么线程就会阻塞等待。
int sem_wait(sem_t *sem); //P()

发布信号量
功能:发布信号量,表示资源使用完毕,可以归还资源了。将信号量值加1。若信号量没达到最大值,则说明有现成正在阻塞等待,V操作就会唤醒一个线程。
int sem_post(sem_t *sem);//V()

于是我们就可以定义两个信号量
N表示环形队列(缓冲区)中能存放的空间数量
默认:DataSem初始生产数据值为0

注意:
在这里插入图片描述

在这里插入图片描述

代码如下(省略Task.hpp

//ringQueue

#pragma once
#include <iostream>
#include <vector>
#include <semaphore.h>

const static int defaultcap = 5;

template<class T>
class RingQueue{
private:
    //申请信号量
    void P(sem_t &sem)
    {
        sem_wait(&sem);
    }
    //释放信号量
    void V(sem_t &sem)
    {
        sem_post(&sem);
    }
    void Lock(pthread_mutex_t &mutex)
    {
        pthread_mutex_lock(&mutex);
    }
    void Unlock(pthread_mutex_t &mutex)
    {
        pthread_mutex_unlock(&mutex);
    }
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 &in) 
    {
        //判断: 申请空间信号量(失败阻塞)
        P(pspace_sem_);
        //加锁
        Lock(p_mutex_);
        ringqueue_[p_step_] = in;

        p_step_++;
        p_step_ %= cap_;
        
        Unlock(p_mutex_);
        //释放数据信号量
        V(cdata_sem_);
    }
    //消费
    void Pop(T *out)       
    {
        //判断: 申请数据信号量(失败阻塞)
        P(cdata_sem_);
        //加锁
        Lock(c_mutex_);
        *out = ringqueue_[c_step_];

        c_step_++;
        c_step_ %= cap_;

        Unlock(c_mutex_);
        //释放空间信号量
        V(pspace_sem_);
    }
    ~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_;
};
//main.cc

#include "ringQueue.hpp"
#include "Task.hpp"
#include <stdlib.h>
#include <unistd.h>
#include <string>

struct ThreadData
{
    RingQueue<Task> *rq;        //指向环形队列的指针
    std::string threadname;     //线程名
};

//生产
void* Productor(void* args)
{
    ThreadData *td = static_cast<ThreadData *>(args);
    RingQueue<Task> *rq = td->rq;
    //sleep(5);
    while(true)
    {
        int number1 = rand() % 10 + 1;
        usleep(10);
        //让随机数number2出现0的情况
        int number2 = rand() % 10;
        int r = rand() % 5;
        Task t(number1, number2, oper_[r]);
        rq->Push(t);     //传入任务信息
        std::cout <<"我是:" << td->threadname << " " <<"任务信息: " << t.GetTask() << std::endl;
        sleep(1);
    }
    return nullptr;
}
//消费
void* Consumer(void* args)
{
    ThreadData *td = static_cast<ThreadData *>(args);
    RingQueue<Task> *rq = td->rq;
    Task t;
    while(true)
    {
        rq->Pop(&t);
        t();
        std::cout <<"我是:" << td->threadname <<" "<< " result: " << t.GetResult() << std::endl;
        sleep(1);
    }
    return nullptr;
}

int main()
{
    //创建指向阻塞队列的指针(其中队列中放着一个个Task对象(任务))
    srand(time(nullptr) ^ getpid());
    RingQueue<Task> *rq = new RingQueue<Task>(50);

    pthread_t c[3], p[5];
    for (int i = 0; i < 5; i++)
    {
        ThreadData *td = new ThreadData();
        td->rq = rq;
        td->threadname = "Productor-" + std::to_string(i);

        pthread_create(p + i, nullptr, Productor, td);
    }
    for (int i = 0; i < 3; i++)
    {
        ThreadData *td = new ThreadData();
        td->rq = rq;
        td->threadname = "Consumer-" + std::to_string(i);

        pthread_create(c + i, nullptr, Consumer, td);
    }

    for (int i = 0; i < 5; i++)
    {
        pthread_join(p[i], nullptr);
    }
    for (int i = 0; i < 3; i++)
    {
        pthread_join(c[i], nullptr);
    }
    delete rq;

    return 0;
}

运行结果
在这里插入图片描述

小结

今日的分享就到这里啦,如果本文存在疏漏或错误的地方,还请您能够指出!
在这里插入图片描述

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

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

相关文章

OJ_快速幂

分解幂计算再加和 递推数列 核心&#xff1a;求方阵的幂 #include <iostream>using namespace std;//矩阵乘法 void MatrixMultiply(int m1[2][2],int m2[2][2],int res[2][2]){res[0][0] (m1[0][0] * m2[0][0] %10000) (m1[0][1] * m2[1][0] %10000);res[0][0] % 10…

记录一个vue,ele-ui实现列表指定行数批量选中解决方法

这个问题卡了一天&#xff0c;试了好多方法总算试出来了&#xff1a; <template><div><!-- 功能区卡片 --><el-card class"mb-4"><el-row class"mb-1"><el-col :span"12">请输入想勾选的专利起止条数&am…

python基础 | 核心库:NumPy 矩阵计算

NumPy不是标准库&#xff0c;不是自带的&#xff0c;需要自己安装。要通过终端来安装&#xff0c;vs里面的不行 官方文档 1、创建 1.1 指定创建 import numpy as npa np.array([1,2,3]) # 创建数组(以列表方式)# 注&#xff1a;asarray 和array类似&#xff0c;只是array会…

Spring Boot项目中使用MyBatis连接达梦数据库6

在开发中&#xff0c;使用Spring Boot框架结合MyBatis来操作数据库是一种常见的做法。本篇博客将介绍如何在Spring Boot项目中配置MyBatis来连接达梦数据库6&#xff0c;并提供一个简单的示例供参考。(达梦六不仅分表还分模式.) 我拿SYSTEM表的LPS模式下面Student表做案例。 1.…

C语言自定义类型结构体

variable adj.易变的&#xff0c;多变的&#xff1b;时好时坏的&#xff1b;可变的&#xff0c;可调节的&#xff1b; &#xff08;数&#xff09;&#xff08;数字&#xff09;变量的&#xff1b;&#xff08;植&#xff0c;动&#xff09;变异的&#xff0c;变型的&#xff1…

【阿里云物联网】上报设备数据

前言 MQTT客户端上传数据到阿里云服务端&#xff0c;并且能将数据显示出来。在此之前&#xff0c;我们先要懂得阿里云给设备管理划分的概念。首先是产品&#xff0c;所以在产品里要配置内容&#xff0c;产品下的设备才可以使用&#xff0c;比如主题大类都是在产品里面就可以查…

优惠:阿里云4核16G服务器优惠价格26.52元1个月、149.00元半年

阿里云4核16G服务器优惠价格26.52元1个月、79.56元3个月、149.00元半年&#xff0c;配置为阿里云服务器ECS经济型e实例ecs.e-c1m4.xlarge&#xff0c;4核16G、按固定带宽 10Mbs、100GB ESSD Entry系统盘&#xff0c;活动链接 aliyunfuwuqi.com/go/aliyun 活动链接打开如下图&a…

(一)基于IDEA的JAVA基础5

Scanner的使用 使用scanner可以接收键盘上输入的数据&#xff0c; Scanner inputnew Scanner(System.in)&#xff1b; 导包的方式: 什么是导包&#xff0c;导入的是jdk提供的java开发工具包&#xff0c;我们建一个java文件&#xff0c;psvm快捷输入后&#xff0c;打上new S…

有没有适合pr剪辑视频使用的蓝色魔法火焰能量特效素材模板

12个蓝色魔法火焰能量特效VFX元素pr素材模板。 可定制的能量电荷、灰尘等离子体和发光的电火花是游戏电影、电影特效或有影响力的视频内容的理想选择&#xff0c;增添了史诗般的电影质量。无论是神秘的爆炸、闪闪发光的闪电还是旋转的漩涡&#xff0c;每一部动画都是力量的灯塔…

力扣 柱形图中最大的矩形 单调栈

84. 柱状图中最大的矩形 - 力扣&#xff08;LeetCode&#xff09; 这篇文章讲的非常棒 class Solution { public:int largestRectangleArea(vector<int>& heights) {int n1heights.size(),ans0,i;新建数组&#xff0c;长度是heights.size()2第一位和最后一位为0 …

四种最新算法(小龙虾优化算法COA、螳螂搜索算法MSA、红尾鹰算法RTH、霸王龙优化算法TROA)求解机器人路径规划(提供MATLAB代码)

一、机器人路径规划介绍 移动机器人&#xff08;Mobile robot&#xff0c;MR&#xff09;的路径规划是 移动机器人研究的重要分支之&#xff0c;是对其进行控制的基础。根据环境信息的已知程度不同&#xff0c;路径规划分为基于环境信息已知的全局路径规划和基于环境信息未知或…

如果用java使用es

添加依赖 如何连接es客户端 RestHighLevelClient 代表是高级客户端 其中hostname&#xff1a;es的服务器地址&#xff0c;prot端口号 &#xff0c;scheme&#xff1a;http还是https 如果不在使用es可以进行关闭&#xff0c;可以防止浪费一些资源 java如何创建索引&#xff1…

Python PyQt5

实现界面开发&#xff0c;与tkinter功能一致&#xff0c;网上已有详细资料&#xff0c;此处仅记录自己的代码&#xff1a; 文章目录 1. 实操1.1 main.py1.2. 窗体模块代码1.3. 页面效果 2. 参考资料2.1. PyQt5 参考资料2.2. tkinter 参考资料 3. 安装注意事项3.1. 下载3.2 Pyc…

双碳目标下基于“遥感+”融合技术在碳储量、碳收支、碳循环等多领域监测与模拟

原文链接&#xff1a;双碳目标下基于“遥感”融合技术在碳储量、碳收支、碳循环等多领域监测与模拟https://mp.weixin.qq.com/s?__bizMzUzNTczMDMxMg&mid2247598506&idx6&snde95898e0b6017271a3b0bfbacc1f034&chksmfa82004dcdf5895bf44730ef2e6a5e8ee590cca1a…

Windows server 2012 R2系统怎么显示桌面图标

当我们在使用Windows server2012 R2服务器计算机时&#xff0c;为了方便&#xff0c;我们可以添加桌面图标。下面就给大家分享一下添加桌面图标的方法&#xff1b; 操作步骤如下&#xff1a; 1、第一步&#xff0c;我们打开server服务器&#xff0c;就可以看到如下画面&#x…

【python】python种子数据集——聚类分析建模(源码+数据集)【独一无二】

&#x1f449;博__主&#x1f448;&#xff1a;米码收割机 &#x1f449;技__能&#x1f448;&#xff1a;C/Python语言 &#x1f449;公众号&#x1f448;&#xff1a;测试开发自动化【获取源码商业合作】 &#x1f449;荣__誉&#x1f448;&#xff1a;阿里云博客专家博主、5…

如何做接口测试?

今天来聊聊接口测试&#xff0c;现在是2024年了&#xff0c;打开招聘网站随便点开一个招聘帖子&#xff0c;几乎都可以看到岗位JD要求写着有接口测试经验优先。其重要性可见一斑&#xff01; 目前&#xff0c;凡是好一点稍具规模的公司哪怕是大厂外包也几乎都要求会接口测试&a…

深度学习入门指南:从理论到实践

深度学习如何入门 深度学习是机器学习的一个分支&#xff0c;它通过模拟人脑神经网络的结构和功能来实现对数据的学习和理解。近年来&#xff0c;深度学习在图像识别、自然语言处理、语音识别等领域取得了显著的成果&#xff0c;越来越受到人们的关注。如果你想入门深度学习&a…

【LLAVA】Llava中在数据集制作过程中是怎么从CC3M中过滤出595K数据的?为什么这样做?

原文&#xff1a;CC3M. We extract noun-phrases using Spacy for each caption over the whole cc3m dataset, and count the frequency of each unique noun-phrase. We skip noun-phrases whose frequency is smaller than 3, as they are usually rare combinations concep…

[MySQL实战] 如何定义唯一约束(唯一索引)

文章目录 一、什么是唯一约束二、如何定义唯一约束2.1、建表时定义唯一约束--方法12.2、建表时定义唯一约束--方法22.3、为已创建的表定义唯一约束 三、删除唯一约束四、问题4.1、问题1&#xff1a;如何为多个列定义唯一约束&#xff1f; 五、总结 一、什么是唯一约束 唯一约束…