【Linux 22】生产者消费者模型

news2024/9/30 5:35:05

文章目录

  • 🌈 一、生产者消费者模型
    • ⭐ 1. 生产者消费者模型的概念
    • ⭐ 2. 生产者消费者模型的特点
    • ⭐ 3. 生产者消费者模型的优点
  • 🌈 二、基于阻塞队列的生产消费模型
    • ⭐ 1. 阻塞队列概念
    • ⭐ 2. 模拟实现基于阻塞队列的生产消费模型
  • 🌈 三、POSIX 信号量
    • ⭐ 1. POSIX 信号量概念
      • 🌙 1.1 信号量的本质
      • 🌙 1.2 信号量的 PV 操作
    • ⭐ 2. POSIX 信号量函数
      • 🌙 2.1 初始化信号量
      • 🌙 2.2 销毁信号量
      • 🌙 2.3 申请信号量 (等待信号量)
      • 🌙 2.4 释放信号量 (发布信号量)
  • 🌈 四、基于环形队列的生产消费模型
    • ⭐ 1. 环形队列的概念
      • 🌙 1.1 空间资源和数据资源
    • ⭐ 2. 环形队列的规则
      • 🌙 2.1 生产者和消费者不能访问同一个位置
      • 🌙 2.2 生产者不能快消费者一圈以上
      • 🌙 2.3 消费者不能超过生产者
    • ⭐ 3. 生产者消费者并发访问环形队列的场景
    • ⭐ 4. 模拟实现基于环形队列的生产消费模型

🌈 一、生产者消费者模型

⭐ 1. 生产者消费者模型的概念

  • 生产者消费者模式通过一个容器来解决生产者和消费者之间的强耦合问题。
  • 生产者和消费者之间不会直接通讯,而是通过这个容器来进行通讯。
  • 生产者生产完数据之后不用等消费者处理,而是直接将生产出的数据放到这个容器,消费者也不找生产者要数据,而是直接从这个容器里取。
    • 简单理解成 供应商 → 超市 → 消费者 就行。
  • 一般是使用阻塞队列 / 环形队列作为生产者和消费者之间通讯的容器,这个容器相当于一个缓冲区,平衡了生产者和消费者的处理能力。
  • 这个阻塞队列就是专门用来给生产者和消费者解耦的。

image-20240917211817739

⭐ 2. 生产者消费者模型的特点

1. 生产者与消费者之间的 321 原则

  • 3 种关系:生产者和生产者 (互斥关系)、消费者和消费者 (互斥关系)、生产者和消费者 (互斥 / 同步关系) 。
  • 2 种角色:生产者和消费者 (通常由 进程 / 线程 充当这两种角色) 。
  • 1 个场所:生产者和消费者交易的场所,通常指的是内存中的一段缓冲区,也能自己通过某种方式组织起来。

2. 为什么生产者和消费者、消费者和消费者、生产者和生产者之间会存在互斥关系

  • 生产者和消费者:生产者在放数据时不知道自己有没有放完数据,消费者在拿数据时也不知道自己有没有拿到数据,如果不互斥,在访问同一个位置时就可能产生数据不一致的问题,因此需要竞争互斥锁。

  • 生产者和生产者:在访问同一个位置时,可能生产者 A、B 都认为这个位置没东西,就会都往这里放数据,会导致先放的数据会被后放的数据覆盖。

  • 消费者和消费者:消费者 A、B 同时访问一个位置,都认为这个位置有数据,但 B 先把东西拿走了,A 不知道数据已经被取走了,继续去拿数据,导致拿了个寂寞。

3. 生产者和消费者之间为什么会存在同步关系

  • 如果让生产者一直生产数据,当容器被数据塞满后,生产者再进行生产就会导致生产失败。
  • 如果让消费者一直拿取数据,当容器数据被搬空后,消费者再进行消费就会导致消费失败。
  • 虽然,一直生产或一直消费不会导致数据不一致的问题,但却会引发饥饿问题。
  • 应该让生产者和消费者之间在访问容器时具有一定的顺序性。
    • 当容器的管理者发现容器内数据量下降到标准线之下时,就让消费者停止消费,通知生产者生产数据。
    • 当容器中的数据量被填充到标准线之上时,就让生产者停止生产,通知消费者过来消费。

4. 互斥保证数据的准确性,同步将多线程协同起来,两者并不冲突

⭐ 3. 生产者消费者模型的优点

1. 解耦

  • 生产者只负责生产数据,消费者只负责消费数据,两者之间互不影响。

  • 从代码层面看,生产者线程和消费者线程的代码并不直接互相调用,两者的代码在发生变化不会对对方造成影响。

2. 支持并发

  • 在消费者从缓冲区拿取数据后的处理数据期间,生产者可以同时进行生产对缓冲区添加数据。
  • 如果没有缓冲区,消费者得直接去找生产者要数据,就必须等待生产真产生数据,同理,生产者也需要等待消费者消费数据。

3. 支持忙闲不均

  • 在缓冲区未满时,生产者和消费者互不影响,不会产生占用 CPU 时间片的问题;
  • 在缓冲区已满时,生产者不再生产数据,在缓冲区空时,消费者不再消费数据,使得两者总体处于一种动态平衡的状态。

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

⭐ 1. 阻塞队列概念

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

image-20240919203925657

阻塞队列和一般队列的区别

  • 当阻塞队列为空时,消费者线程从阻塞队列中获取元素的操作会被阻塞,直到阻塞队列中被生产者线程放入元素为止;
  • 当阻塞队列为满时,生产者线程往阻塞队列中存放数据的操作会被阻塞,直到阻塞队列因为被消费者线程拿走元素而出现空位为止。

⭐ 2. 模拟实现基于阻塞队列的生产消费模型

  • 根据 C++ 中的 queue 容器实现一个阻塞队列。
  • 为了方便演示,实现的是一个单生产者、单消费者的模型。

image-20240919205630182

  • 当生产者线程把阻塞队列填满时,通知消费者线程消费;当消费者线程把阻塞队列搬空时,通知生产者线程生产。
    • 也可以不这么极端,可以设置一个标准水位线,当阻塞队列中的数据量 < 水位线时,让生产者生产数据,> 水位线时,让消费者消费数据。
    • 这里就实现 空 / 满 这种极端做法实现。
  • 注:由生产者线程通知消费者线程消费,由消费者线程通知生产者线程生产。
    • 当消费者线程发现阻塞队列空了之后,就要通知生产者线程生产数据,然后消费者去指定条件变量处等待。
    • 当生产者线程发现阻塞队列满了之后,就要通知消费者线程消费数据,然后生产者去指定条件变量处等待。
#include <queue>
#include <iostream>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>

using std::cout;
using std::endl;
using std::queue;

const int num = 6;          // 设阻塞队列的容量为 6

template<typename T>
class block_queue
{
private:
    queue<T> q;           // 用队列实现阻塞队列, 阻塞队列属于临界资源
    int cap;                // 阻塞队列的容量
    pthread_mutex_t lock;   // 互斥锁, 用于保护使用阻塞队列的临界区
    pthread_cond_t full;    // 生产者线程用的条件变量, 当阻塞队列满时去这里等待
    pthread_cond_t empty;   // 消费者线程用的条件变量, 当阻塞队列空时去这里等待

private:
    // 上锁
    void lock_queue()
    {
        pthread_mutex_lock(&lock);
    }

    // 解锁
    void unlock_queue()
    {
        pthread_mutex_unlock(&lock);
    }

    // 让生产者线程去 full 条件变量处等待
    void product_wait()
    {
        pthread_cond_wait(&full, &lock);
    }

    // 让消费者线程去 empty 条件变量处等待
    void consume_wait()
    {
        pthread_cond_wait(&empty, &lock);
    }

    // 唤醒在 full 条件变量队列等待的的队头的生产者线程
    void notify_product()
    {
        pthread_cond_signal(&full);
    }

    // 唤醒在 empty 条件变量队列等待的的队头的消费者线程
    void notify_consume()
    {
        pthread_cond_signal(&empty);
    }

    // 判断当前阻塞队列是否为空
    bool is_empty()
    {
        return 0 == q.size();
    }

    // 判断当前阻塞队列是否为满
    bool is_full()
    {
        return q.size() == cap;
    }

public:
    // 构造函数
    block_queue(int _cap = num) 
        : cap(_cap)
    {
        pthread_mutex_init(&lock, nullptr);	// 初始化互斥锁
        pthread_cond_init(&full, nullptr);	// 初始化生产者线程用的条件变量
        pthread_cond_init(&empty, nullptr);	// 初始化消费者线程用的条件变量
    }
    
    // 生产者线程往阻塞队列中放数据
    void push_data(const T &data)
    {
        lock_queue();		 	// 下面的代码要访问临界资源 (阻塞队列) 了, 是临界区, 上锁

        while (is_full())       // 阻塞队列满了
        {
            notify_consume();   // 通知消费者线程消费数据
            cout << "阻塞队列满了, 通知消费者消费数据, 让生产者停止生产" << endl;
            product_wait();     // 让生产者线程等待,不要再生产了
        }
        q.push(data);           // 阻塞队列没满时,生产者线程会一直往里面放数据

        unlock_queue();			// 访问完临界区了, 解锁
    }

    // 消费者线程从阻塞队列中拿数据
    void pop_data(T *data)
    {
        lock_queue();			// 下面的代码要访问临界资源 (阻塞队列) 了, 是临界区, 上锁
        
        while (is_empty())  	// 阻塞队列空了
        {
            notify_product();	// 通知生产者线程生产数据
            cout << "阻塞队列空了, 通知生产者生产数据, 让消费者停止消费" << endl;
            consume_wait();		// 让消费者线程等待,不要再拿取数据了
        }
        *data = q.front();		// 阻塞队列没空时,消费者线程会从里面拿取数据
        q.pop();

        unlock_queue();			// 访问完临界区了, 解锁
    }
	
    // 析构函数
    ~block_queue()
    {
        pthread_mutex_destroy(&lock);   // 销毁保护阻塞队列的互斥锁
        pthread_cond_destroy(&full);    // 销毁生产者线程用的条件变量
        pthread_cond_destroy(&empty);   // 销毁消费者线程用的条件变量
    }
};

// 消费者线程调用的函数
void *consumer(void *arg)
{
    int data;
    block_queue<int> *bqp = (block_queue<int> *)arg;

    while (true)
    {
        bqp->pop_data(&data);    // 消费者线程用 data 从阻塞队列获取数据
        cout << "消费者拿取数据完毕, 拿取的数据是: " << data << endl;
        sleep(1);
    }
}

// 生产者线程调用的函数
void *producter(void *arg)
{
    // 生产者线程生产的数据是一些随机数

    block_queue<int> *bqp = (block_queue<int> *)arg;
    srand((unsigned long)time(NULL));       // 随机数种子

    while (true)
    {
        int data = rand() % 1024;           // 随机数
        bqp->push_data(data);               // 将生产者生产的随机数放入阻塞队列
        cout << "生产者生产数据完毕, 生产的数据是: " << data << endl;
        sleep(1);
    }
}

int main()
{
    block_queue<int> bq; 	// 阻塞队列对象
    pthread_t cid;  		// 消费者线程的 id
    pthread_t pid;  		// 生产者线程的 id
	
    // 创建生产者和消费者线程, 让它们去执行各自的函数
    pthread_create(&cid, nullptr, consumer, (void *)&bq);    // 将阻塞队列对象作为参数传递给消费者线程调用的函数
    pthread_create(&pid, nullptr, producter, (void *)&bq);   // 将阻塞队列对象作为参数传递给生产者线程调用的函数
    
    pthread_join(cid, nullptr); // 主线程等待消费者线程
    pthread_join(pid, nullptr); // 主线程等待生产者线程

    return 0;
}

🌈 三、POSIX 信号量

⭐ 1. POSIX 信号量概念

  • POSIX 信号量的作用和 SystemV 信号量相同,都是用于同步操作,从而达到无冲突的访问资源的目的;但 POSIX 信号量可以用于线程间同步

🌙 1.1 信号量的本质

  • POSIX 信号量本质上是一个计数器,用于描述临界资源数量的计数器,能够更细粒度的对临界资源进行管理。

    • 假设当前有一个可以容纳 100 个人的博物馆,信号量的初始值就是 100,表示博物馆可提供的资源数。
    • 每当进去一个人时,信号量的值就减 1;每当出去一个人时,信号量的值就加 1。
    • 当信号量的值为 0 时,说明博物馆已经装不下更多人了,让后面的游客 (线程) 一直等到信号量不为 0。
  • 执行流在进入临界区之前,都应该申请信号量,申请成功后才具备操作临界资源的权限,当操作完毕后也应该释放信号量。

image-20240921104451355

🌙 1.2 信号量的 PV 操作

  • P 操作:将申请信号量称为 P 操作,申请信号量本质就是申请对临界资源的使用权限,当申请成功时会让信号量的值 - 1。因此 P 操作的本质就是让信号量这个计数器的值 - 1。
  • V 操作:将释放信号量成为 V 操作,释放信号量本质就是归还对临界资源的使用权限,当释放成功时会让信号量的值 + 1。因此 V 操作的本质就是让信号量这个计数器的值 + 1。

信号量的 PV 操作必须是原子的

  • 和争锁一样,多执行流之间为了访问临界资源也会竞争信号量,因此信号量也会被多执行流同时访问,即信号量本身就是个临界资源。
  • 但信号量本质上是为了保护临界资源,不可能说再弄个信号量或锁去保护信号量,因此信号量的 PV 操作必须是原子的。

⭐ 2. POSIX 信号量函数

  • 信号量的数据类型是 sem_t,可以使用该类型定义信号量。
  • 信号量函数的返回值:调用函数成功时返回 0,失败时返回 - 1。
  • 在使用信号量函数之前,应该先使用 #include <semaphore.h> 引入库文件。

🌙 2.1 初始化信号量

#include <semaphore.h>

int sem_init(
    	sem_t *sem, 		/* sem 表示需要初始化的信号量 */
    	int pshared, 		/* 设置 sem 的共享方式,为 0 在线程间共享,非 0 在进程间共享 */
    	unsigned int value); /* 信号量 (计数器) 的初始值 */

🌙 2.2 销毁信号量

#include <semaphore.h>

int sem_destroy(sem_t *sem);	// sem 表示要销毁的信号量

🌙 2.3 申请信号量 (等待信号量)

  • 申请信号量 (等待信号量) 就是 PV 操作中的 P 操作。
    • 注:申请信号量的 P 操作应该在线程争锁之前进行,只有在确定有资源的情况下才让线程去争锁。
#include <semaphore.h>

int sem_wait(sem_t *sem);	// sem 表示线程需要申请的信号量
  • 调用该函数时,如果申请信号量成功,则让信号量的值 - 1;
  • 如果申请信号量失败,则将调用该函数的线程在 sem 信号量的等待队列处挂起等待。

🌙 2.4 释放信号量 (发布信号量)

  • 释放信号量 (发布信号量) 就是 PV 操作中的 V 操作。
    • 注:释放信号量的 V 操作应该在线程解锁之后进行。
#include <semaphore.h>

int sem_post(sem_t *sem);	// sem 表示线程需要释放的信号量
  • 调用该函数时,如果释放信号量成功,则让信号量的值 + 1。

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

⭐ 1. 环形队列的概念

  • 环形队列与阻塞队列最大的不同就是,环形队列能够构成一个环。
  • 在环形队列中,生产者和消费者一开始可以指向同一个位置,启动之后,让生产者先生产数据,消费者跟在后面消费数据。

image-20240921141906901

🌙 1.1 空间资源和数据资源

  • 并不一定说,只有数据才是资源,角色的不同,也会导致对资源的认识不同。

  • 对于生产者来说,它关心的是缓冲区的空间资源,而对于消费者来说,它关心的是缓冲区中的数据资源

  • 缓冲区中只要有空间,生产者就能干活;同理,缓冲区中只要有数据,消费者就能干活。

⭐ 2. 环形队列的规则

🌙 2.1 生产者和消费者不能访问同一个位置

  • 如果生产者和消费者访问的是环形队列中相同的位置,就可能会出现数据不一致的问题。
  • 消费者想拿旧数据,生产者新生产出的数据可能会将该位置旧有的数据给覆盖掉,但消费者又不知道自己拿到的是新的数据。

image-20240921143003333

🌙 2.2 生产者不能快消费者一圈以上

  • 如果生产者线程跑的太快了,绕一圈回来撞上在后面拿数据的消费者线程,如果生产者此时还不停下来,就会覆盖掉之前的数据。
  • 这条规则本质上就是生产者和消费者不能访问同一个位置。

image-20240921161121392

🌙 2.3 消费者不能超过生产者

  • 消费者已经将环形队列中的数据消费完了,此时消费者已经追上了生产者。
  • 如果消费者想超过生产者继续往前走,前面就不会有数据可供消费者消费了。
  • 就算前面有数据,消费者拿到的也是之前用过的旧数据,并不是所需的新数据。

image-20240921192649973

⭐ 3. 生产者消费者并发访问环形队列的场景

1. 以下情况会导致生产者和消费者访问同一个位置,不能实现并发访问

  1. 环形队列为空:消费者消费了一圈追上了生产者,生产者和消费者访问的是同一个位置,此时消费者应该停止消费让生产者去生产

  2. 环形队列为满:生产者生产力一圈追上了消费者,生产者和消费者访问的是同一个位置,此时生产者应该停止生产让消费者去消费

2. 生产者和消费者不访问同一位置时,可以实现并发访问

  • 生产者在前面跑,消费者在后面追,两者之间的距离小于一圈。
  • 这样可以让生产者一直能够获取空间资源,让消费者一直能够获取数据资源。

⭐ 4. 模拟实现基于环形队列的生产消费模型

  • 想要判断环形队列是否为 空 / 满,可以通过计数器。也可以预留一个位置作为满的状态。
  • 在了解了信号量之后,就可以使用信号量作为环形队列中资源数量的计数器
    • 可以定义两个信号量,分别是给生产者用的空间资源信号量,以及给消费者用的数据资源信号量
  • 当前要实现的是让生产者生产者先生产一个随机数,然后让消费者消费这个随机数,生产者与消费者之间就差一个身位。
#include <vector>
#include <iostream>
#include <unistd.h>
#include <pthread.h>
#include <semaphore.h>

using std::cout;
using std::endl;
using std::vector;

const int default_cap = 4;              // 环形队列的默认大小

// 定义并初始化一个生产者之间的互斥锁
pthread_mutex_t p_mutex = PTHREAD_MUTEX_INITIALIZER;

// 定义并初始化一个消费者之间的互斥锁
pthread_mutex_t c_mutex = PTHREAD_MUTEX_INITIALIZER;

template<typename T>
class ring_queue
{
private:
    vector<T> q;                        // 用数组来作为表示环形队列的容器
    int cap;                            // 环形队列的基本容量
    sem_t data_sem;                     // 给消费者使用的数据资源信号量
    sem_t space_sem;                    // 给生产者使用的空间资源信号量
    int consumer_step;                  // 记录消费者要往环形队列中 拿 数据的位置
    int producer_step;                  // 记录生产者要从环形队列中 放 数据的位置

public:
    ring_queue(int _cap = default_cap) 
        : q(_cap), cap(_cap)
    {
        sem_init(&data_sem, 0, 0);      // 初始化数据信号量,数据信号量的初始值为 0
        sem_init(&space_sem, 0, cap);   // 初始化空间信号量,空间信号量的初始值为环形队列的大小
        consumer_step = 0;              // 消费者线程最开始从环形队列中 拿 数据的位置
        producer_step = 0;              // 生产者线程最开始往环形队列中 放 数据的位置
    }

    // 生产者线程往环形队列中添加数据
    void put_data(const T &data)
    {
        sem_wait(&space_sem);           // P 操作: 少了一个空间资源,空间信号量的值 - 1

        pthread_mutex_lock(&p_mutex);   // 上锁, 所有申请到信号量的线程中,只允许一个去添加数据
        
        q[consumer_step] = data;        // 往环形队列中添加数据
        consumer_step++;                // 走到下一个要放数据的位置
        consumer_step %= cap;           // 防止越界
        
        pthread_mutex_unlock(&p_mutex); // 解锁
        
        sem_post(&data_sem);            // V 操作: 多个一个数据资源,数据信号量的值 + 1
    }

    // 消费者线程从环形队列中获取数据
    void get_data(T* data)
    {
        sem_wait(&data_sem);            // P 操作: 少了一个数据资源,数据信号量的值 - 1
        
        pthread_mutex_lock(&c_mutex);   // 上锁, 所有申请到信号量的线程中,只允许一个去获取数据
        
        *data = q[producer_step];       // 从环形队列中拿取数据
        producer_step++;                // 走到下一个要拿数据的位置
        producer_step %= cap;           // 防止越界
        
        pthread_mutex_unlock(&c_mutex); // 解锁
        
        sem_post(&space_sem);           // V 操作: 多了一个空间资源,空间信号量的值 + 1
    }

    ~ring_queue()
    {
        sem_destroy(&data_sem);         // 销毁消费者使用的数据信号量
        sem_destroy(&space_sem);        // 销毁生产者使用的空间信号量
    }
};

// 消费者线程调用的函数
void *consumer(void *arg)
{
    int data;
    ring_queue<int> *rqp = (ring_queue<int> *)arg;
    
    while (true)
    {
        rqp->get_data(&data);   // 从环形队列中获取数据

        // 显示器也是临界资源,也需要用锁保护起来
        pthread_mutex_lock(&p_mutex);
        cout << "consumer get data: " << data << endl;
        pthread_mutex_unlock(&p_mutex);

        sleep(1);
    }
}

// 生产者线程调用的函数
void *producer(void *arg)
{
    ring_queue<int> *rqp = (ring_queue<int> *)arg;
    srand((unsigned long)time(nullptr));

    while (true)
    {
        int data = rand() % 1024;
        rqp->put_data(data);    // 往环形队列中添加数据

        // 显示器也是临界资源,也需要用锁保护起来
        pthread_mutex_lock(&c_mutex);
        cout << "producer put data: " << data << endl;
        pthread_mutex_unlock(&c_mutex);

        sleep(1);
    }
}

// 主线程
int main()
{
    ring_queue<int> rq; // 定义环形队列对象
    pthread_t cid;      // 记录消费者线程的 ID
    pthread_t pid;      // 记录生产者线程的 ID

    // 创建生产者和消费者线程,并让两种线程调用指定函数
    pthread_create(&cid, nullptr, consumer, (void *)&rq);   // 创建消费者线程
    pthread_create(&pid, nullptr, producer, (void *)&rq);   // 创建生产者线程
    
    pthread_join(cid, nullptr); // 主线程等待消费者线程退出
    pthread_join(pid, nullptr); // 主线程等待生产者线程退出

    return 0;
}

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

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

相关文章

Kubernetes云原生存储解决方案之 Rook Ceph实践探究

Kubernetes云原生存储解决方案之 Rook Ceph实践探究 除了手动部署独立的 Ceph 集群并配置与Kubernetes进行对接外&#xff0c;Rook Ceph 支持直接在 Kubernetes 集群上部署 Ceph 集群。 通过Rook Ceph云原生存储编排平台&#xff0c;使得 Kubernetes 集群中启用高可用的 Ceph…

PHP 基础语法详解

✅作者简介&#xff1a;2022年博客新星 第八。热爱国学的Java后端开发者&#xff0c;修心和技术同步精进。 &#x1f34e;个人主页&#xff1a;Java Fans的博客 &#x1f34a;个人信条&#xff1a;不迁怒&#xff0c;不贰过。小知识&#xff0c;大智慧。 &#x1f49e;当前专栏…

【有啥问啥】多目标跟踪SORT算法原理详解

多目标跟踪SORT算法原理详解 引言 多目标跟踪&#xff08;Multiple Object Tracking, MOT&#xff09;是计算机视觉领域的一个重要研究方向&#xff0c;广泛应用于视频监控、自动驾驶、人机交互等多个领域。其核心任务是在视频序列中持续、准确地识别和定位多个目标。SORT&am…

爬虫入门之爬虫原理以及请求响应

爬虫入门之爬虫原理以及请求响应 爬虫需要用到的库, 叫requests. 在导入requests库之前, 需要安装它, 打开cmd: 输入pip install 库名 pip install requests后面出现successful或requirement already就说明已经下载成功了!!! 下载出现的问题: 1.有报错或者是下载慢 修改镜像…

计算机的错误计算(一百零八)

摘要 回复网友来信&#xff0c;接前一节本节再谈多项式的错误计算。 例1. 计算 若在Visual Studio 2010中用C#编程计算&#xff1a; using System; using System.Collections.Generic; using System.Linq; class Program { static void Main(){ long part1 946495 * (…

Redis缓存双写一致性笔记(下)

Redis和Canal结合使用是一种常见的解决方案&#xff0c;用于确保MySQL数据库中的更改实时同步到Redis缓存中&#xff0c;从而保持数据的一致性。 这种同步机制虽然能够实现近乎实时的数据同步&#xff0c;但可能会有轻微的延迟&#xff0c;因此它更适合对数据一致性要求不是特…

STM32 DMA+AD多通道

单片机学习&#xff01; 目录 一、DMA配置步骤 二、ADC配置步骤 三、DMAAD多通道框图 四、DMAAD多通道函数设计详细步骤 4.1 开启RCC时钟 4.2 配置GPIO 4.3 配置多路开关 4.4 结构体初始化ADC 4.5 DMA参数初始化配置 4.5.1 外设站点的三个参数 4.5.2 存储器站点的三个…

Tomcat 调优技巧(Tomcat Tuning Tips)

&#x1f49d;&#x1f49d;&#x1f49d;欢迎来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:Linux运维老纪的首页…

IDEA关联Tomcat

一、Tomcat服务器 web服务器,就是运行web项目的容器 即运行java代码的一个容器 webapp(web应用程序) --> 就是我们写的javaweb项目 Tomcat 是Apache 软件基金会&#xff08;Apache Software Foundation&#xff09;下的一个核心项目&#xff0c;免费开源、并支持Servlet 和J…

yolov8/9/10模型在垃圾分类检测中的应用【代码+数据集+python环境+GUI系统】

yolov8/9/10模型在垃圾分类检测中的应用【代码数据集python环境GUI系统】 yolov8/9/10模型在垃圾分类检测中的应用【代码数据集python环境GUI系统】 背景意义 随着计算机视觉技术和深度学习算法的快速发展&#xff0c;图像识别、对象检测、图像分割等技术在各个领域得到了广泛…

DL_语义分割(学习笔记)

文章目录 图像分割1 常见分类1.1 语义分割1.2 实例分割1.3 全景分割 2 语义分割2.1 模型评价指标2.2 常用数据集2.3 转置卷积2.4 膨胀卷积2.5 感受野2.6 双线性插值2.7 FCN 图像分割 1 常见分类 1.1 语义分割 定义&#xff1a;【只判断类别&#xff0c;无法区分个体】 语义分…

Matlab实现麻雀优化算法优化回声状态网络模型 (SSA-ESN)(附源码)

目录 1.内容介绍 2.部分代码 3.实验结果 4.内容获取 1内容介绍 麻雀搜索算法&#xff08;Sparrow Search Algorithm, SSA&#xff09;是一种新兴的群体智能优化算法&#xff0c;灵感来源于麻雀的觅食行为及其在面临危险时的预警机制。SSA通过模拟麻雀的这些自然行为来寻找问题…

[Docker学习笔记]利用Dockerfile创建镜像

Dockerfile 指令 指令作用from继承基础镜像maintainer镜像制作者信息(可缺省)run用来执行shell命令expose暴露端口号cmd启动容器默认执行的命令entrypoint启动容器真正执行的命令volume创建挂载点env配置环境变量add复制文件到容器copy复制文件到容器workdir设置容器的工作目录…

蓝卓亮相中国工博会,打造以数据驱动的智能工厂

9月28日&#xff0c;以“工业聚能&#xff0c;新质领航”为主题的第24届中国国际工业博览会&#xff08;以下简称“工博会”&#xff09;在国家会展中心&#xff08;上海&#xff09;圆满拉下帷幕。本届工博会共设9大专业展区&#xff0c;吸引了来自全球28个国家和地区的2600余…

针对考研的C语言学习(定制化快速掌握重点4)

typedef的使用 简化变量类型 逻辑结构 集合结构&#xff1a;无关系 线性结构&#xff1a;一对一 树形结构&#xff1a;一对多 图形结构&#xff1a;多对多 存储结构 顺序存储和链式存储&#xff08;考代码&#xff09; 顺序存储优点&#xff1a;1.可以实现随机存取。2.…

针对考研的C语言学习(定制化快速掌握重点5)

顺序表 特点&#xff1a; 写代码主要就是增删改查&#xff01;&#xff01;&#xff01; 写代码的边界性非常重要以及考研插入和删除的位置都是从1开始&#xff0c;而数组下标是从0开始 【注】下标和位置的关系 线性表最重要的是插入和删除会涉及边界问题以及判断是否合法 …

【Spring Boot 入门二】Spring Boot中的配置文件 - 掌控你的应用设置

一、引言 在上一篇文章中&#xff0c;我们开启了Spring Boot的入门之旅&#xff0c;成功构建了第一个Spring Boot应用。我们从环境搭建开始&#xff0c;详细介绍了JDK的安装以及IDE的选择与配置&#xff0c;然后利用Spring Initializr创建了项目&#xff0c;分析了项目结构&am…

资质申请中常见的错误有哪些?

在申请建筑资质的过程中&#xff0c;企业可能会犯一些常见的错误&#xff0c;以下是一些需要避免的错误&#xff1a; 1. 资料准备不充分&#xff1a; 申请资质需要提交大量的资料&#xff0c;包括企业法人资料、财务报表、业绩证明等。资料不齐全或不准确都可能导致申请失败。…

多线程(一):线程的基本特点线程安全问题ThreadRunnable

目录 1、线程的引入 2、什么是线程 3、线程的基本特点 4、线程安全问题 5、创建线程 5.1 继承Thread类&#xff0c;重写run 5.1.1 创建Thread类对象 5.1.2 重写run方法 5.1.3 start方法创建线程 5.1.4 抢占式执行 5.2 实现Runnable&#xff0c;重写run【解耦合】★…

MySQL-数据库设计

1.范式 数据库的范式是⼀组规则。在设计关系数据库时&#xff0c;遵从不同的规范要求&#xff0c;设计出合理的关系型数 据库&#xff0c;这些不同的规范要求被称为不同的范式。 关系数据库有六种范式&#xff1a;第⼀范式&#xff08;1NF&#xff09;、第⼆范式&#xff08;…