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

目录

        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 效果展示

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

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

相关文章

Vue3快速上手(五)ref之对象类型的响应式数据

一、ref之对象类型的响应式数据 1.1 基本语法 import { ref } from vuelet x ref(初始值)console.log(xxx --> , x.value);x为一个RefImpl对象&#xff0c;该对象的value属性为实际值&#xff0c;在script里需要操作x.value来改变数据的值&#xff0c;在页面里则可以直接…

常用算法介绍-->快速排序

本篇文章我们来介绍一下快速排序的算法 1.简介 快速排序是对冒泡排序的一种改进&#xff0c; 它是不稳定的。由C. A. R. Hoare在1962年提出的一种划分交换排序&#xff0c;采用的是分治策略&#xff08;一般与递归结合使用&#xff09;&#xff0c;以减少排序过程中的比较次数…

PHP脉聊交友系统网站源码,可通过广告变现社交在线聊天交友即时通讯APP源码,附带视频搭建教程

探索全新社交体验&#xff1a;一站式PHP交友网站解决方案 &#x1f310; 全球化交友&#xff0c;无界沟通 在数字化的浪潮下&#xff0c;社交已不再受地域限制。我们的PHP交友网站不仅支持多国语言&#xff0c;还配备了即时翻译功能&#xff0c;让您轻松跨越语言障碍&#xff…

Pure Pursuit控制器路径跟随

参考博客&#xff1a; Pure Pursuit 纯追踪法 Autoware(Pure pursuit代码学习) 1 Pure Pursuit纯追踪法 适用场景&#xff1a;低速场景&#xff08;速度过高会产生转弯内切以及超调&#xff09; 简化前轮转向角和后轴将遵循的曲率之间的关系 (Gx,Gy)是下一个需要追踪的路点&a…

【实战】一、Jest 前端自动化测试框架基础入门(二) —— 前端要学的测试课 从Jest入门到TDD BDD双实战(二)

文章目录 一、Jest 前端自动化测试框架基础入门5.Jest 中的匹配器toBe 匹配器toEqual匹配器toBeNull匹配器toBeUndefined匹配器和toBeDefined匹配器toBeTruthy匹配器toBeFalsy匹配器数字相关的匹配器字符串相关的匹配器数组相关的匹配器异常情况的匹配器 6.Jest 命令行工具的使…

C++【AVL树】

目录 1.AVL树&#xff08;高度平衡搜索二叉树&#xff09;定义 1.1平衡因子&#xff08;Balance Factor简写成bf&#xff09; 1.2avl树的定义 2.AVL树插入的基本操作 2.1逻辑抽象图 2.2插入流程 2.3左单旋 2.4右单旋 2.5右左双旋 2.6左右双旋 3.AVL树的检验技巧 3.1…

AI:127-基于卷积神经网络的交通拥堵预测

🚀点击这里跳转到本专栏,可查阅专栏顶置最新的指南宝典~ 🎉🎊🎉 你的技术旅程将在这里启航! 从基础到实践,深入学习。无论你是初学者还是经验丰富的老手,对于本专栏案例和项目实践都有参考学习意义。 ✨✨✨ 每一个案例都附带有在本地跑过的关键代码,详细讲解供…

2024-02-13 Unity 编辑器开发之编辑器拓展4 —— EditorGUIUtility

文章目录 1 EditorGUIUtility 介绍2 加载资源2.1 Eidtor Default Resources2.2 不存在返回 null2.3 不存在则报错2.4 代码示例 3 搜索框查询、对象选中提示3.1 ShowObjectPicker3.2 PingObject3.3 代码示例 4 窗口事件传递、坐标转换4.1 CommandEvent4.2 GUIPoint 和 ScreenPoi…

Excel模板2:进度条甘特图

Excel模板2&#xff1a;进度条甘特图 ‍ 今天复刻B站up【名字叫麦兜的狗狗】的甘特图&#xff1a;还在买Excel模板吗&#xff1f;自己做漂亮简洁的甘特图吧&#xff01;_哔哩哔哩_bilibili 阿里网盘永久分享&#xff1a;https://www.alipan.com/s/cXhq1PNJfdm 当前效果&…

片上网络NoC(5)——非直连拓扑

目录 一、前言 二、概念阐述 三、交叉开关 四、蝶形网络 五、clos网络 六、fat tree网络 6.1 clos网络的折叠过程 七、总结 一、前言 本文继续介绍片上网络的拓扑&#xff0c;在之前的文章中&#xff0c;我们已经介绍了片上网络的拓扑指标和直连拓扑的相关内容&#xf…

【新年第一辑 | TortoiseGit常见用法】

TortoiseGit常见用法 概述常用操作建立仓库提交代码更新代码回滚版本添加忽略文件设置比较工具&#x1fa78; 解决冲突 主页传送门 &#xff1a; &#x1f4c0; 传送 概述 TortoiseGit是一个Windows平台上的Git客户端工具&#xff0c;它提供了一个直观和易于使用的图形用户界面…

面向对象2:继承

目录 2.1继承 2.2 继承的好处 2.3 权限修饰符 2.4 单继承、Object 2.5 方法重写 2.6 子类中访问成员的特点 2.7 子类中访问构造器的特点 面向对象1&#xff1a;静态 2.1继承 向对象编程之所以能够能够被广大开发者认可&#xff0c;有一个非常重要的原因&#xff0c;是…

springboot184基于springboot的校园网上店铺的设计与实现

简介 【毕设源码推荐 javaweb 项目】基于springbootvue 的 适用于计算机类毕业设计&#xff0c;课程设计参考与学习用途。仅供学习参考&#xff0c; 不得用于商业或者非法用途&#xff0c;否则&#xff0c;一切后果请用户自负。 看运行截图看 第五章 第四章 获取资料方式 **项…

如何把华为手机上的数据转移到荣耀手机上?

方法/步骤 点击并进入华为手机&#xff08;旧手机&#xff09;的【手机克隆】应用&#xff0c;选择【这是旧设备】&#xff1b; 点击并进入荣耀手机&#xff08;新手机&#xff09;的【换机克隆】应用&#xff0c;选择【这是新设备】&#xff1b; 荣耀手机&#xff08;新…

LeetCode-第70题-爬楼梯

1.题目描述 假设你正在爬楼梯。需要 n 阶你才能到达楼顶。 每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢&#xff1f; 2.样例描述 3.思路描述 画图就可以发现规律&#xff0c;典型的斐波那契额数列 4.代码展示 class Solution {public int climbStair…

webgis后端安卓系统部署攻略,超详细Termux攻略

目录 前言 一、将后端项目编译ARM64 二、安卓手机安装termux 1.更换为国内源 2.安装ssh远程访问 3.安装文件远程访问 三、安装postgis数据库 1.安装数据库 2.数据库配置 3.数据导入 四、后端项目部署 五、自启动设置 总结 前言 因为之前一直做的H5APP开发&#xf…

机器学习、深度学习、强化学习、迁移学习的关联与区别

Hi&#xff0c;大家好&#xff0c;我是半亩花海。本文主要了解并初步探究机器学习、深度学习、强化学习、迁移学习的关系与区别&#xff0c;通过清晰直观的关系图展现出四种“学习”之间的关系。虽然这四种“学习”方法在理论和应用上存在着一定的区别&#xff0c;但它们之间也…

2024幻兽帕鲁服务器创建教程_阿里PK腾讯超简单

幻兽帕鲁官方服务器不稳定&#xff1f;自己搭建幻兽帕鲁服务器&#xff0c;低延迟、稳定不卡&#xff0c;目前阿里云和腾讯云均推出幻兽帕鲁专用服务器&#xff0c;腾讯云直接提供幻兽帕鲁镜像系统&#xff0c;阿里云通过计算巢服务&#xff0c;均可以一键部署&#xff0c;鼠标…

深度学习-吴恩达L1W2作业

作业1&#xff1a;吴恩达《深度学习》L1W2作业1 - Heywhale.com 作业2&#xff1a;吴恩达《深度学习》L1W2作业2 - Heywhale.com 作业1 你需要记住的内容&#xff1a; -np.exp&#xff08;x&#xff09;适用于任何np.array x并将指数函数应用于每个坐标 -sigmoid函数及其梯度…

【编程题】合法括号的判断

合法括号的判断—难度&#xff1a;⭐⭐ 我的答案&#xff1a;错误 class Parenthesis {public:bool chkParenthesis(string A, int n) { // write code hereif (n % 2 ! 0) {return false;}stack<char> st;auto ch A.begin(); // cout<<"hello?"<&l…