【Linux学习】生产者-消费者模型

news2025/1/17 0:12:53

目录

        22.1 什么是生产者-消费者模型

        22.2 为什么要用生产者-消费者模型?

        22.3 生产者-消费者模型的特点

        22.4 BlockingQueue实现生产者-消费者模型

        22.4.1 实现阻塞队列BlockQueue

                1) 添加一个容器来存放数据

                2)加入判断Blocking Queue情况的成员函数

                3)实现push和pop方法

                4)完整代码

        22.4.2  [可选] 修改成RAII风格代码

        22.4.3 定义Blocking Queue中存放Task类任务

        22.4.4 生产者-消费者模型主函数实现

                1) 实现主函数

                2)定义任务函数

                3)定义消费者函数 consumer,生产者函数 producer

                4)完整代码

        22.4.5 makefile编译

        22.4.6 效果展示


        22.1 什么是生产者-消费者模型

生产者 - 消费者模型( Producer-consumer problem) 是一个非常经典的多线程并发协作的模型

​在这个模型中,有两种角色:

  • 生产者:生成数据并将其放入共享资源中
  • 消费者:从共享资源中获取数据并进行处理。

它们共享一个有限的资源,比如一个缓冲区。

我们可以用超市购物的场景来解释生产者-消费者模型:

  • 生产者:在这个例子中,生产者是超市的供应商。他们将各种商品(产品)送到超市的货架上,让消费者购买。供应商不断地提供新货物并放置在货架上。
  • 共享资源:在这个例子中,共享资源就是超市的货架。货架有限,无法容纳无限数量的商品。因此,货架可以看作是一个有界缓冲区,只能容纳一定数量的商品。
  • 消费者:消费者是超市的顾客。他们来到超市,从货架上选购商品,并将其购买。消费者会不断地从货架上取走商品。
  • 潜在问题:货架容量有限,供应商不能无限制地往货架上放商品,否则会导致货架满了,无法再放入新商品。同样,如果货架上没有商品了,顾客无法购买商品,会感到不满。超市需要协调供应商和顾客的行为。
  • 解决方案:供应商必须在货架有空间时才能往货架上放置商品,否则需要等待。顾客只有在货架上有商品时才能选购,否则需要等待。这种协调可以通过合适的管理和排队机制来实现,以确保货架的正常供应和顾客的购买需求。

        22.2 为什么要用生产者-消费者模型?

  • 缓冲和平衡负载

在多线程开发中,为了解决生产者和消费者之间速度不匹配的问题,常常会引入一个缓冲区来平衡生产和消费的速度差异。

缓冲区的作用是暂时存储生产者生产的数据,以便消费者在需要时取出。这样一来,即使生产者的速度比消费者快,生产者也不需要等待消费者立即处理数据,而是可以继续生产新的数据并将其放入缓冲区。同样,如果消费者的速度比生产者快,消费者也可以从缓冲区中取出数据并进行处理,而不必等待新数据的到来。

  • 解耦生产者和消费者

生产者和消费者可以独立运行,彼此之间无需直接交互。这种解耦可以简化系统的设计和维护,并且允许更容易地修改或替换生产者和消费者的实现。

        22.3 生产者-消费者模型的特点

多线程同步与互斥:生产者消费者模型是一个典型的多线程同步与互斥场景。多个生产者和消费者之间需要同步操作共享资源,同时确保互斥访问,避免数据竞争和不一致状态。

  • 三种关系

    • 生产者与生产者之间存在互斥关系:多个生产者不能同时往共享资源中添加数据,需要通过互斥机制保证只有一个生产者访问资源。
    • 消费者与消费者之间存在互斥关系:多个消费者不能同时从共享资源中取出数据,也需要通过互斥机制保证只有一个消费者访问资源。
    • 生产者与消费者之间存在互斥关系和同步关系:生产者生产数据后需要通知消费者进行消费,消费者消费完数据后需要通知生产者进行生产。这种同步关系确保生产者和消费者之间的顺序执行。
  • 两种角色:生产者和消费者是模型中的两种核心角色,通常由线程或进程来扮演。生产者负责生成数据并放入共享资源,而消费者负责从共享资源中取出数据并进行处理。

  • 一个交易场所:共享资源通常是一个缓冲区,用于暂时存储生产者生产的数据,以便消费者进行消费。这个交易场所可以是内存中的一段缓冲区,也可以是其他形式的数据结构,如队列、管道等。

我们用代码编写生产者消费者模型的时候,本质就是对这三个特点进行维护。

        22.4 BlockingQueue实现生产者-消费者模型

22.4.1 实现阻塞队列BlockQueue

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

阻塞队列为什么适用于实现生产者和消费者模型:

  • 当队列为空时,从队列获取元素的操作将会被阻塞,直到队列中放入了元素。
  • 当队列满时,往队列里存放元素的操作会被阻塞,直到有元素从队列中取出。

实现阻塞队列的基本原理:

  1. 阻塞队列通过使用互斥锁和条件变量来确保对队列的访问是线程安全的。互斥锁用于保护对队列的并发访问,而条件变量用于在适当的时候通知等待的线程。

  2. 当生产者要向队列中放入数据时,首先会获取互斥锁,以确保在放入数据的过程中不会被其他线程干扰。然后,生产者会检查队列是否已满,如果队列已满,则生产者会等待条件变量,直到队列有空闲空间为止。

  3. 同样地,当消费者要从队列中取出数据时,也会先获取互斥锁,以确保在取出数据的过程中不会被其他线程干扰。然后,消费者会检查队列是否为空,如果队列为空,则消费者会等待条件变量,直到队列中有数据可取。

  4. 这种同步和互斥机制确保了生产者和消费者之间的顺序执行。生产者和消费者之间通过条件变量进行通信,生产者负责向队列中放入数据,消费者负责从队列中取出数据,二者之间通过互斥锁确保对队列的安全访问。


介绍完原理,我们开始一步一步用代码来实现

1) 添加一个容器来存放数据

我们使用STL中现成的queue来模拟实现Blocking Queue ,这里我们创建一个名为BlockQueue.hpp的文件来定义BlockingQueue类

const int gDefaultCap = 5;
template <class T>
class BlockQueue
{
public:
    BlockQueue(int capacity = gDefaultCap) : capacity_(capacity)
    {
        pthread_mutex_init(&mtx_, nullptr);
        pthread_cond_init(&Empty_, nullptr);
        pthread_cond_init(&Full_, nullptr);
    }
    ~BlockQueue()
    {
        pthread_mutex_destroy(&mtx_);
        pthread_cond_destroy(&Empty_);
        pthread_cond_destroy(&Full_);
    }
private:
    std::queue<T> bq_;     // 阻塞队列
    int capacity_;         // 容量上限
    pthread_mutex_t mtx_;  // 通过互斥锁保证队列安全
    pthread_cond_t Empty_; // 用它来表示bq 是否空的条件
    pthread_cond_t Full_;  //  用它来表示bq 是否满的条件
};

这里我们默认capacity为5,具体可以通过修改gDefaultCap改变

2)加入判断Blocking Queue情况的成员函数
bool isQueueEmpty()
{
    return bq_.size() == 0;
}
bool isQueueFull()
{
    eturn bq_.size() == capacity_;
}

isQueueEmpty()判断队列是否为空:

当消费者试图从队列中取出数据时,如果队列为空,则消费者需要等待直到队列中有数据可取,以避免消费者线程空转浪费资源。

isQueueFull()判断队列是否已满:

当生产者试图向队列中放入数据时,如果队列已满,则生产者需要等待直到队列有空闲位置,以避免向已满的队列中添加数据。

3)实现push和pop方法
 void push(const T &in) // 生产者
    {
        pthread_mutex_lock(&mtx_);
        while(isQueueFull()) pthread_cond_wait(&Full_, &mtx_);
        bq_.push(in);
        if(bq_.size() >= capacity_/2) pthread_cond_signal(&Empty_);
        pthread_mutex_unlock(&mtx_);
    } 
    void pop(T *out)
    {
        pthread_mutex_lock(&mtx_);
        while (isQueueEmpty())
        pthread_cond_wait(&Empty_, &mtx_);
        *out = bq_.front();
        bq_.pop();
        pthread_cond_signal(&Full_);
        pthread_mutex_unlock(&mtx_);
    }

判断是否满足生产消费条件时不能用if,而应该用while:

  • pthread_cond_wait函数是让当前执行流进行等待的函数,是函数就意味着有可能调用失败,调用失败后该执行流就会继续往后执行。
  • 其次,在多消费者的情况下,当生产者生产了一个数据后如果使用pthread_cond_broadcast函数唤醒消费者,就会一次性唤醒多个消费者,但待消费的数据只有一个,此时其他消费者就被伪唤醒了。
  • 为了避免出现上述情况,我们就要让线程被唤醒后再次进行判断,确认是否真的满足生产消费条件,因此这里必须要用while进行判断。
4)完整代码
#pragma once
#include <iostream>
#include <queue>
#include <mutex>
#include <pthread.h>
const int gDefaultCap = 5;
template <class T>
class BlockQueue
{
private:
    bool isQueueEmpty()
    {
        return bq_.size() == 0;
    }
    bool isQueueFull()
    {
        return bq_.size() == capacity_;
    }

public:
    BlockQueue(int capacity = gDefaultCap) : capacity_(capacity)
    {
        pthread_mutex_init(&mtx_, nullptr);
        pthread_cond_init(&Empty_, nullptr);
        pthread_cond_init(&Full_, nullptr);
    }
    void push(const T &in) // 生产者
    {
        pthread_mutex_lock(&mtx_);
        while(isQueueFull()) pthread_cond_wait(&Full_, &mtx_);
        bq_.push(in);
        if(bq_.size() >= capacity_/2) pthread_cond_signal(&Empty_);
        pthread_mutex_unlock(&mtx_);
    } 
    void pop(T *out)
    {
        pthread_mutex_lock(&mtx_);
        while (isQueueEmpty())
            pthread_cond_wait(&Empty_, &mtx_);
        *out = bq_.front();
        bq_.pop();
        pthread_cond_signal(&Full_);
        pthread_mutex_unlock(&mtx_);
    }
    ~BlockQueue()
    {
        pthread_mutex_destroy(&mtx_);
        pthread_cond_destroy(&Empty_);
        pthread_cond_destroy(&Full_);
    }
private:
    std::queue<T> bq_;     // 阻塞队列
    int capacity_;         // 容量上限
    pthread_mutex_t mtx_;  // 通过互斥锁保证队列安全
    pthread_cond_t Empty_; // 用它来表示bq 是否空的条件
    pthread_cond_t Full_;  //  用它来表示bq 是否满的条件
};

22.4.2  [可选] 修改成RAII风格代码

我们可以定义了一个 lockGuard 类,采用 RAII(资源获取即初始化)方式,对互斥锁进行加锁和解锁,确保在作用域结束时自动释放锁。

这里我们创建一个名为lockGuard.hpp:的文件来定义lockGuard类

#pragma once
#include <iostream>
#include <pthread.h>
class lockGuard
{
public:
    lockGuard(pthread_mutex_t *mtx) : mtx_(mtx)
    {
        pthread_mutex_lock(mtx_);
    }

    ~lockGuard()
    {
        pthread_mutex_unlock(mtx_);
    }

private:
    pthread_mutex_t *mtx_; // 指向要管理的互斥锁的指针
};

lockGuard类的构造函数中,首先通过传入的pthread_mutex_t类型的指针初始化mtx_成员变量,即指向要管理的互斥锁。然后调用pthread_mutex_lock函数对该互斥锁进行加锁操作。

lockGuard类的析构函数中,调用pthread_mutex_unlock函数对互斥锁进行解锁操作。由于该析构函数在对象生命周期结束时自动调用,因此实现了互斥锁的自动释放。这样,在使用lockGuard对象时,只需要在作用域中创建该对象,当对象离开作用域时,析构函数会自动调用,从而释放互斥锁,确保了互斥锁的安全管理。

修改后的Blocking Queue代码

#pragma once
#include <iostream>
#include <queue>
#include <mutex>
#include <pthread.h>
#include "lockGuard.hpp"
const int gDefaultCap = 5;
template <class T>
class BlockQueue
{
private:
    bool isQueueEmpty()
    {
        return bq_.size() == 0;
    }
    bool isQueueFull()
    {
        return bq_.size() == capacity_;
    }
public:
    BlockQueue(int capacity = gDefaultCap) : capacity_(capacity)
    {
        pthread_mutex_init(&mtx_, nullptr);
        pthread_cond_init(&Empty_, nullptr);
        pthread_cond_init(&Full_, nullptr);
    }
    void push(const T &in) // 生产者
    {
        lockGuard lockgrard(&mtx_); // 自动调用构造函数

        while (isQueueFull())
            pthread_cond_wait(&Full_, &mtx_);
        bq_.push(in);
        if(bq_.size() >= capacity_/2) pthread_cond_signal(&Empty_);
    } // 自动调用lockgrard 析构函数
    void pop(T *out)
    {
        lockGuard lockguard(&mtx_);
        while (isQueueEmpty())
            pthread_cond_wait(&Empty_, &mtx_);
        *out = bq_.front();
        bq_.pop();
        pthread_cond_signal(&Full_);
    }
    ~BlockQueue()
    {
        pthread_mutex_destroy(&mtx_);
        pthread_cond_destroy(&Empty_);
        pthread_cond_destroy(&Full_);
    }
private:
    std::queue<T> bq_;     // 阻塞队列
    int capacity_;         // 容量上限
    pthread_mutex_t mtx_;  // 通过互斥锁保证队列安全
    pthread_cond_t Empty_; // 用它来表示bq 是否空的条件
    pthread_cond_t Full_;  //  用它来表示bq 是否满的条件
};

22.4.3 定义Blocking Queue中存放Task类任务

现在我么已经实现了BlockQueue的逻辑,但是我们需要实现生产者生产资源后通过阻塞队列派发给消费者,这里我们不妨将派发的资源定义为一个Task类,生产者将Task任务派发给消费者完成

这里我们创建一个名为Task.hpp的文件来定义Task类

#pragma once
#include <iostream>
#include <functional>
typedef std::function<int(int, int)> func_t;
class Task
{
public:
    Task(){}
    Task(int x, int y, func_t func):x_(x), y_(y), func_(func)
    {}
    int operator ()()
    {
        return func_(x_, y_);
    }
public:
    int x_;
    int y_;
    func_t func_;
};

重载了函数调用运算符 operator(),使得 Task 类的对象可以像函数一样被调用。在这个运算符重载函数中,调用了成员变量 func_ 所指向的函数对象,并传入 x_y_ 作为参数,返回函数调用的结果。

22.4.4 生产者-消费者模型主函数实现

这里我们创建一个名为pro-con.cc的文件来模拟实现生产者-消费者模型

1) 实现主函数
int main()
{
    srand((uint64_t)time(nullptr) ^ getpid() ^ 0x32457);
    BlockQueue<Task> *bqueue = new BlockQueue<Task>();
    pthread_t c[2],p[2];
    pthread_create(p, nullptr, productor, bqueue);
    pthread_create(p + 1, nullptr, productor, bqueue);
    sleep(1);
    pthread_create(c, nullptr, consumer, bqueue);
    pthread_create(c + 1, nullptr, consumer, bqueue);
    pthread_join(c[0], nullptr);
    pthread_join(c[1], nullptr);
    pthread_join(p[0], nullptr);
    pthread_join(p[1], nullptr);
    delete bqueue;
    return 0;
}
  • srand((uint64_t)time(nullptr) ^ getpid() ^ 0x32457);:用于初始化随机数生成器的种子。
  • BlockQueue<Task> *bqueue = new BlockQueue<Task>();:创建了一个 BlockQueue 类型的阻塞队列对象。
  • pthread_create:创建了两个消费者线程和两个生产者线程,并分别传入相应的函数指针和参数。
  • pthread_join:等待所有线程的完成。
  • delete bqueue;:释放了动态分配的阻塞队列对象的内存空间。
2)定义任务函数

我们设计的任务函数是两个参数的类型,为了方便演示,这里我们就简单写了一个加法Add函数来实现(有兴趣可以自己DIY!) 

int myAdd(int x, int y)
{
    return x + y;
}
3)定义消费者函数 consumer,生产者函数 producer
void* consumer(void *args)
{
    BlockQueue<Task> *bqueue = (BlockQueue<Task> *)args;
    while(true)
    {
        Task t;
        bqueue->pop(&t);
        std::cout << pthread_self() <<" consumer: "<< t.x_ << "+" << t.y_ << "=" << t() << std::endl;
    }
    return nullptr;
}
void* productor(void *args)
{
    BlockQueue<Task> *bqueue = (BlockQueue<Task> *)args;
    while(true)
    {
        int x = rand()%10 + 1;
        usleep(rand()%1000);
        int y = rand()%5 + 1;
        Task t(x, y, myAdd);
        bqueue->push(t);
        std::cout <<pthread_self() <<" productor: "<< t.x_ << "+" << t.y_ << "=?" << std::endl;
        sleep(1);
    }
    return nullptr;
}
  • void* consumer(void *args):消费者线程的入口函数。它接收一个 BlockQueue<Task> 类型的参数,并不断地从阻塞队列中取出任务对象,并执行任务函数。执行完毕后,打印出任务的计算结果。
  • void* productor(void *args):生产者线程的入口函数。它接收一个 BlockQueue<Task> 类型的参数,并不断地生成随机的任务对象,并将其推送到阻塞队列中。每个任务对象都包含两个随机生成的整数参数和任务函数的指针。生产者线程每次生成任务后,都会打印出任务的描述。
4)完整代码
#include "BlockQueue.hpp"
#include <pthread.h>
#include <unistd.h>
#include <ctime>
#include "Task.hpp"
int myAdd(int x, int y)
{
    return x + y;
}
void* consumer(void *args)
{
    BlockQueue<Task> *bqueue = (BlockQueue<Task> *)args;
    while(true)
    {
        Task t;
        bqueue->pop(&t);
        std::cout << pthread_self() <<" consumer: "<< t.x_ << "+" << t.y_ << "=" << t() << std::endl;
    }
    return nullptr;
}
void* productor(void *args)
{
    BlockQueue<Task> *bqueue = (BlockQueue<Task> *)args;
    while(true)
    {
        int x = rand()%10 + 1;
        usleep(rand()%1000);
        int y = rand()%5 + 1;
        Task t(x, y, myAdd);
        bqueue->push(t);
        std::cout <<pthread_self() <<" productor: "<< t.x_ << "+" << t.y_ << "=?" << std::endl;
        sleep(1);
    }
    return nullptr;
}

int main()
{
    srand((uint64_t)time(nullptr) ^ getpid() ^ 0x32457);
    BlockQueue<Task> *bqueue = new BlockQueue<Task>();
    pthread_t c[2],p[2];
    pthread_create(p, nullptr, productor, bqueue);
    pthread_create(p + 1, nullptr, productor, bqueue);
    sleep(1);
    pthread_create(c, nullptr, consumer, bqueue);
    pthread_create(c + 1, nullptr, consumer, bqueue);
    pthread_join(c[0], nullptr);
    pthread_join(c[1], nullptr);
    pthread_join(p[0], nullptr);
    pthread_join(p[1], nullptr);
    delete bqueue;
    return 0;
}

22.4.5 makefile编译

pro-con:pro-con.cc
	g++ -o $@ $^ -std=c++11 -lpthread
.PHONY:clean
clean:
	rm -f pro-con

22.4.6 效果展示

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

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

相关文章

使用word2vec+tensorflow自然语言处理NLP

目录 介绍&#xff1a; 搭建上下文或预测目标词来学习词向量 建模1&#xff1a; 建模2&#xff1a; 预测&#xff1a; 介绍&#xff1a; Word2Vec是一种用于将文本转换为向量表示的技术。它是由谷歌团队于2013年提出的一种神经网络模型。Word2Vec可以将单词表示为高维空间…

JAVA-多进程开发-创建等待进程

前言 在项目中&#xff0c;为了实现“并发编程”&#xff08;同时执行多个任务&#xff09;&#xff0c;就引入了“多进程编程”&#xff0c;把一个很大的任务&#xff0c;拆分成若干个很小的任务&#xff0c;创建多个进程&#xff0c;每个进程分别负责其中的一部分任务。 这也…

Java实现中学生家校互联系统 JAVA+Vue+SpringBoot+MySQL

目录 一、摘要1.1 项目介绍1.2 项目录屏 二、功能模块2.1 学生管理模块2.2 课堂表现模块2.3 考试成绩模块2.4 家校留言模块2.5 校园通知模块 三、系统设计3.1 用例设计3.2 实体类设计3.2.1 课堂表现实体类设计3.2.2 考试成绩实体类设计3.2.3 家校留言实体类设计3.2.4 校园通知实…

虚拟人专题报告:虚拟人深度产业分析报告

今天分享的是虚拟人系列深度研究报告&#xff1a;《虚拟人专题报告&#xff1a;虚拟人深度产业分析报告》。 &#xff08;报告出品方&#xff1a;Q量子位&#xff09; 报告共计&#xff1a;18页 技术背景 虚拟数字人指存在于非物理世界中&#xff0c;由计算机图形学、图形渲…

【Vue】工程化开发脚手架Vue CLI

&#x1f4dd;个人主页&#xff1a;五敷有你 &#x1f525;系列专栏&#xff1a;Vue⛺️稳重求进&#xff0c;晒太阳 工程化开发&脚手架Vue CLI 基本介绍 Vue Cli是Vue官方提供的一个全局命令工具 可以帮助我们快速创建一个开发Vue项目的标准化基础架子【集成了we…

【小沐学GIS】基于C++QT绘制三维数字地球Earth(OpenGL)

&#x1f37a;三维数字地球系列相关文章如下&#x1f37a;&#xff1a;1【小沐学GIS】基于C绘制三维数字地球Earth&#xff08;456:OpenGL、glfw、glut&#xff09;第一期2【小沐学GIS】基于C绘制三维数字地球Earth&#xff08;456:OpenGL、glfw、glut&#xff09;第二期3【小沐…

四、OpenAI之文本生成模型

文本生成模型 OpenAI的文本生成模型(也叫做生成预训练的转换器(Generative pre-trained transformers)或大语言模型)已经被训练成可以理解自然语言、代码和图片的模型。模型提供文本的输出作为输入的响应。对这些模型的输入内容也被称作“提示词”。设计提示词的本质是你如何对…

python 经典老人言

python 经典老人言 import tkinter as tkclass FlipBook:def __init__(self, master):self.master master master.title("经 典 老 人 言")self.pages ["经 典 老 人 言","求学无笨者&#xff0c;努力就成功","读 书 百 遍&am…

代码随想录|day 16

Day 16 迎财神 坚持如此hard 玄之又玄&#xff0c;众妙之门 一、理论知识 回顾【深度】和【高度】的概念&#xff0c;现在主要还是写递归2&#xff09; 初识【回溯】3&#xff09;左叶子的明确定义&#xff1a;节点A的左孩子不为空&#xff0c;且左孩子的左右孩子都为空&am…

Python学习之路-爬虫提高:scrapy基础

Python学习之路-爬虫提高:scrapy基础 为什么要学习scrapy 通过前面的学习&#xff0c;我们已经能够解决90%的爬虫问题了&#xff0c;那么scrapy是为了解决剩下的10%的问题么&#xff0c;不是&#xff0c;scrapy框架能够让我们的爬虫效率更高 什么是scrapy Scrapy是一个为了…

(三十七)大数据实战——Solr服务的部署安装

前言 Solr是一个基于Apache Lucene的开源搜索平台&#xff0c;它提供了强大的全文搜索、分布式搜索和数据分析功能。Solr 可以用于构建高性能的搜索应用程序&#xff0c;支持从海量数据中快速检索和分析信息。Solr 使用倒排索引和先进的搜索算法&#xff0c;可实现快速而准确的…

AI绘画作品的展示和变现-2

4.7 制作红包封面 中国的节日和传统文化元素仍然可以成为创作者们的创作灵感&#xff0c;创造出更多的变现机会。比如元宵节&#xff0c;可以制作大型元宵图案&#xff0c;进行引流并卖出元宵。 而春分、谷雨等节气也可以成为创作的灵感来源&#xff0c;创作出与之相关的图案&…

最新wordpress外贸主题

日用百货wordpress外贸主题 蓝色大气的wordpress外贸主题&#xff0c;适合做日用百货的外贸公司搭建跨境电商网站使用。 https://www.jianzhanpress.com/?p5248 添加剂wordpress外贸建站主题 橙色wordpress外贸建站主题&#xff0c;适合做食品添加剂或化工添加剂的外贸公司…

软考25-上午题-图2

一、图的存储 图的基本存储结构&#xff1a;邻接矩阵、邻接链表。 1-1、邻接矩阵 用矩阵来表示图中顶点之间的关系。 示例1&#xff1a;有向图的邻接矩阵 示例2&#xff1a;无向图的邻接矩阵 无向图的邻接矩阵是对称的&#xff01;&#xff01;&#xff01; 借助邻接矩阵可…

Linux系统之部署File Browser文件管理系统

Linux系统之部署File Browser文件管理系统 一、File Browser介绍1.1 File Browser简介1.2 File Browser功能1.3 File Browser使用场景 二、本地环境介绍2.1 本地环境规划2.2 本次实践介绍 三、检查本地环境3.1 检查本地操作系统版本3.2 检查系统内核版本 四、安装File Browser4…

66万个 全国行政区划代码表

66万个全国各级行政区划代码表 提供的数据一览 简介 一共有66万个全国各级行政区划&#xff0c;一共有5个级别的行政单位级别 表格头部数据 表格尾部数据 全国行政单位各省份数量统计 数据下载地址 数据整理不易 百度云盘 链接: https://pan.baidu.com/s/1o1C2piYj2wu…

vue3 Element Plus 基于webstorm练习

提要 vue是前端框架&#xff0c;Elemen是组件库。前端框架和组件库的区别与联系 nodejs 脚本语言需要一个解析器才能运行&#xff0c;JavaScript是脚本语言&#xff0c;在不同的位置有不一样的解析器&#xff0c;如写入html的js语言&#xff0c;浏览器是它的解析器角色。而对…

配备Apple T2 安全芯片的 Mac 机型及T2芯片mac电脑U盘装系统教程

T2 芯片为 Mac 提供了一系列功能&#xff0c;例如加密储存和安全启动功能、增强的图像信号处理功能&#xff0c;以及适用于触控 ID 数据的安全保护功能。哪些电脑配备了 T2 安全芯片呢&#xff0c;T2芯片mac电脑又如何重装系统呢&#xff1f;跟随小编一起来看看吧&#xff01; …

测试西门子博途S7-PLCSIM Advanced V5.0的使用

原创 honeytree 西门子博途S7-PLCSIM Advanced V5.0能支持S7-1500&#xff0c;S7-1500R/H&#xff0c;ET200SP&#xff0c;ET200pro的仿真&#xff0c;用此仿真器可以模拟实际的PLC&#xff0c;用于其他软件的连接&#xff0c;比如上位机软件、触摸屏软件,自己用高级语音开发…

415. Add Strings(字符串相加)

问题描述 给定两个字符串形式的非负整数 num1 和num2 &#xff0c;计算它们的和并同样以字符串形式返回。 你不能使用任何內建的用于处理大整数的库&#xff08;比如 BigInteger&#xff09;&#xff0c; 也不能直接将输入的字符串转换为整数形式。 问题分析 按照数字相加以…