当前位置: 首页 > article >正文

【Linux】多线程 -> 线程同步与基于BlockingQueue的生产者消费者模型

线程同步

条件变量

当一个线程互斥地访问某个变量时,它可能发现在其它线程改变状态之前,它什么也做不了。

例如:一个线程访问队列时,发现队列为空,它只能等待,直到其它线程将一个节点添加到队列中。这种情况就需要用到条件变量。

同步概念

同步:在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问题。

竞态条件:因为时序问题,而导致程序异常,我们称之为竞态条件。在线程场景下,这种问题也不难理解。

条件变量函数

初始化

int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict

attr);

参数:

cond:要初始化的条件变量

attr:NULL

销毁

int pthread_cond_destroy(pthread_cond_t *cond);

等待条件满足

int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);

参数:

cond:要在这个条件变量上等待

mutex:互斥量,后面详细解释

唤醒等待

int pthread_cond_broadcast(pthread_cond_t *cond);//唤醒一批线程。

int pthread_cond_signal(pthread_cond_t *cond);//唤醒一个线程。

示例代码:

makefile:

testCond:testCond.ccg++ -o $@ $^ -std=c++11 -lpthread
.PHONY:clean
clean:rm -f testCond

testCond.cc:

#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <string>int tickets = 1000;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;void *start_routine(void *args)
{std::string name = static_cast<const char *>(args);while (true){pthread_mutex_lock(&mutex);pthread_cond_wait(&cond, &mutex);// 判断暂时省略std::cout << name << "->" << tickets << std::endl;tickets--;pthread_mutex_unlock(&mutex);}
}int main()
{// 通过条件变量控制线程的执行pthread_t t[4];for (int i = 0; i < 4; i++){char *name = new char[64];snprintf(name, 64, "thread %d", i + 1);pthread_create(t + i, nullptr, start_routine, (void *)name);}while (true){sleep(1);//pthread_cond_broadcast(&cond); // 唤醒一批线程pthread_cond_signal(&cond);//唤醒一个线程std::cout << "main thread wakeup one thread... " << std::endl;}for (const auto &i : t){pthread_join(i, nullptr);}return 0;
}

pthread_cond_signal:唤醒一个线程。      pthread_cond_broadcast:唤醒一批线程。

这些线程会持续等待一个条件变量的信号。主线程每隔 1 秒就会发送一个条件变量信号,唤醒其中一个等待的线程。被唤醒的线程会输出当前剩余的票数并将票数减 1。

可以看到,由于条件变量的存在,输出结果变得有顺序性。

  • 为什么 pthread_cond_wait 需要互斥量?

1. 保证条件检查和等待操作的原子性

在多线程环境中,线程需要先检查某个条件是否满足,如果不满足则进入等待状态。这个检查条件和进入等待的操作必须是原子的,否则可能会出现竞态条件

例如,在生产者 - 消费者模型中,消费者线程需要检查缓冲区是否为空,如果为空则等待。假设没有互斥量保护,可能会出现以下情况:

  • 消费者线程检查到缓冲区为空,准备进入等待状态。

  • 在消费者线程真正进入等待状态之前,生产者线程往缓冲区中添加了数据,并发出了条件变量的通知。

  • 消费者线程此时才进入等待状态,由于之前通知已经发出,消费者线程可能会一直等待下去,导致程序出现错误。

使用互斥量可以保证条件检查和进入等待状态这两个操作的原子性。当线程调用pthread_cond_wait时,它会先释放互斥量,然后进入等待状态;当被唤醒时,又会重新获取互斥量。这样就避免了上述竞态条件的发生。

2. 保护共享资源和条件变量

条件变量通常与共享资源相关联,线程在检查条件和修改共享资源时需要保证线程安全。互斥量可以用来保护这些共享资源,确保同一时间只有一个线程能够访问和修改它们

在调用pthread_cond_wait之前,线程需要先获取互斥量,这样可以保证在检查条件和进入等待状态时,其他线程不会同时修改共享资源和条件变量。当线程被唤醒后,再次获取互斥量,又可以保证在处理共享资源时的线程安全

生产者消费者模型

  • 为何要使用生产者消费者模型?

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

“321”原则(便于记忆)

  • 3种关系:生产者和生产者(互斥关系)、消费者和消费者(互斥关系)、生产者和消费者(互斥、同步)。
  • 2种角色:生产者线程和消费者线程。
  • 1种交易场所:一段特定结构的缓冲区。

优点

  1. 解耦:生产线程和消费线程解耦。
  2. 支持忙闲不均:生产和消费的一段时间的忙闲不均。
  3. 提高效率:支持并发。

基于BlockingQueue的生产消费模型

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

下面我们以单生产者,单消费者为例:

makefile:

MainCp:MainCp.ccg++ -o $@ $^ -std=c++11 -lpthread
.PHONY:clean
clean:rm -f MainCp

BlockQueue.hpp:

#include <iostream>
#include <queue>
#include <pthread.h>const int gmaxcap = 5;template <class T>
class BlockQueue
{
public:BlockQueue(const int &maxcap = gmaxcap) : _maxcap(maxcap){pthread_mutex_init(&_mutex, nullptr);pthread_cond_init(&_pcond, nullptr);pthread_cond_init(&_ccond, nullptr);}void push(const T &in) // 输入型参数,const &;输出型参数 *;输入输出型参数 &;{pthread_mutex_lock(&_mutex);// 1.判断while (is_full())// if(is_full())//细节2:充当判断的语法必须是while,不能是if,因为在被唤醒时,有可能存在异常或伪唤醒。{// 细节1:pthread_cond_wait是在临界区啊。// pthread_cond_wait的第二个参数,必须是我们正在使用的互斥锁。// a.pthread_cond_wait:该函数调用的时候,会以原子性的方式,将锁释放,并将自己挂起。// b.pthread_cond_wait: 该函数在被唤醒返回的时候,会自动的重新获取你传入的锁。pthread_cond_wait(&_pcond, &_mutex); // 因为生产条件不满足,无法生产,生产者进行等待。}// 2.走到这里,一定是没有满的。_q.push(in);// 3.一定能保证阻塞队列里有数据。// 细节3:pthread_cond_signal:可以放在临界区内部,也可以放在外部。pthread_cond_signal(&_ccond); // 唤醒消费者消费。这里可以有一定的策略。pthread_mutex_unlock(&_mutex);// pthread_cond_siganl(&_ccond);}void pop(T *out){pthread_mutex_lock(&_mutex);// 1.判断while (is_empty())// if(is_empty()){pthread_cond_wait(&_ccond, &_mutex); // 因为消费条件不满足,无法消费,消费者进行等待。}// 2.走到这里,一定是不为空的。*out = _q.front();_q.pop();// 3.一定能保证阻塞队列里至少有一个空位置。pthread_cond_signal(&_pcond); // 唤醒生产者生产。这里可以有一定的策略。pthread_mutex_unlock(&_mutex);}~BlockQueue(){pthread_mutex_destroy(&_mutex);pthread_cond_destroy(&_pcond);pthread_cond_destroy(&_ccond);}private:bool is_empty(){return _q.empty();}bool is_full(){return _q.size() == _maxcap;}private:std::queue<T> _q;int _maxcap; // 队列中元素的上限pthread_mutex_t _mutex;pthread_cond_t _pcond; // 生产者对应的条件变量pthread_cond_t _ccond; // 消费者对应的条件变量
};

 MainCp.cc:

#include "BlockQueue.hpp"
#include <ctime>
#include <sys/types.h>
#include <unistd.h>void *consumer(void *bq_)
{BlockQueue<int> *bq = static_cast<BlockQueue<int> *>(bq_);while (true){// 生产活动int data;bq->pop(&data);std::cout << "消费数据:" << data << std::endl;}return nullptr;
}
void *productor(void *bq_)
{BlockQueue<int> *bq = static_cast<BlockQueue<int> *>(bq_);while (true){// 生产活动int data = rand() % 10 + 1; // 这里我们先用一个随机数构建一个数据。bq->push(data);std::cout << "生产数据:" << data << std::endl;}return nullptr;
}int main()
{srand((unsigned long)time(nullptr) ^ getpid());BlockQueue<int> *bq = new BlockQueue<int>();pthread_t c, p;// 生产消费要看到同一份资源,也就是阻塞队列pthread_create(&c, nullptr, consumer, bq);pthread_create(&p, nullptr, productor, bq);pthread_join(c, nullptr);pthread_join(p, nullptr);delete bq;return 0;
}

如果不加控制,生产消费就会疯狂的执行,没有顺序。

  • 你怎么证明它是一个阻塞队列呢?

让生产者每隔一秒生产一个,消费者一直消费。那么最终的预期结果就是生产一个,消费一个;生产一个,消费一个。

void *consumer(void *bq_)
{BlockQueue<int> *bq = static_cast<BlockQueue<int> *>(bq_);while (true){// 生产活动int data;bq->pop(&data);std::cout << "消费数据:" << data << std::endl;}return nullptr;
}
void *productor(void *bq_)
{BlockQueue<int> *bq = static_cast<BlockQueue<int> *>(bq_);while (true){// 生产活动int data = rand() % 10 + 1; // 这里我们先用一个随机数构建一个数据。bq->push(data);std::cout << "生产数据:" << data << std::endl;sleep(1);}return nullptr;
}

让消费者每隔一秒消费一个,生产者一直生产。那么最终的预期结果就是消费一个,生产一个;消费一个,生产一个。

void *consumer(void *bq_)
{BlockQueue<int> *bq = static_cast<BlockQueue<int> *>(bq_);while (true){// 生产活动int data;bq->pop(&data);std::cout << "消费数据:" << data << std::endl;sleep(1);}return nullptr;
}
void *productor(void *bq_)
{BlockQueue<int> *bq = static_cast<BlockQueue<int> *>(bq_);while (true){// 生产活动int data = rand() % 10 + 1; // 这里我们先用一个随机数构建一个数据。bq->push(data);std::cout << "生产数据:" << data << std::endl;}return nullptr;
}

这就是基于阻塞队列的生产消费模型。

上面我们阻塞队列里放的就是一个整形数据,我们可以再完善一下。我们是可以直接在阻塞队列中放任务的。让生产者给消费者派发任务。

makefile:

MainCp:MainCp.ccg++ -o $@ $^ -std=c++11 -lpthread
.PHONY:clean
clean:rm -f MainCp

BlockQueue.hpp:

#include <iostream>
#include <queue>
#include <pthread.h>const int gmaxcap = 5;template <class T>
class BlockQueue
{
public:BlockQueue(const int &maxcap = gmaxcap) : _maxcap(maxcap){pthread_mutex_init(&_mutex, nullptr);pthread_cond_init(&_pcond, nullptr);pthread_cond_init(&_ccond, nullptr);}void push(const T &in) // 输入型参数,const &;输出型参数 *;输入输出型参数 &;{pthread_mutex_lock(&_mutex);// 1.判断while (is_full())// if(is_full())//细节2:充当判断的语法必须是while,不能是if,因为在被唤醒时,有可能存在异常或伪唤醒。eg:一个生产者十个消费者,broadcast唤醒。{// 细节1:pthread_cond_wait是在临界区啊。// pthread_cond_wait的第二个参数,必须是我们正在使用的互斥锁。// a.pthread_cond_wait:该函数调用的时候,会以原子性的方式,将锁释放,并将自己挂起。// b.pthread_cond_wait: 该函数在被唤醒返回的时候,会自动的重新获取你传入的锁。pthread_cond_wait(&_pcond, &_mutex); // 因为生产条件不满足,无法生产,生产者进行等待。}// 2.走到这里,一定是没有满的。_q.push(in);// 3.一定能保证阻塞队列里有数据。// 细节3:pthread_cond_signal:可以放在临界区内部,也可以放在外部。pthread_cond_signal(&_ccond); // 唤醒消费者消费。这里可以有一定的策略。pthread_mutex_unlock(&_mutex);// pthread_cond_siganl(&_ccond);}void pop(T *out){pthread_mutex_lock(&_mutex);// 1.判断while (is_empty())// if(is_empty()){pthread_cond_wait(&_ccond, &_mutex); // 因为消费条件不满足,无法消费,消费者进行等待。}// 2.走到这里,一定是不为空的。*out = _q.front();_q.pop();// 3.一定能保证阻塞队列里至少有一个空位置。pthread_cond_signal(&_pcond); // 唤醒生产者生产。这里可以有一定的策略。pthread_mutex_unlock(&_mutex);}~BlockQueue(){pthread_mutex_destroy(&_mutex);pthread_cond_destroy(&_pcond);pthread_cond_destroy(&_ccond);}private:bool is_empty(){return _q.empty();}bool is_full(){return _q.size() == _maxcap;}private:std::queue<T> _q;int _maxcap; // 队列中元素的上限pthread_mutex_t _mutex;pthread_cond_t _pcond; // 生产者对应的条件变量pthread_cond_t _ccond; // 消费者对应的条件变量
};

Task.hpp:

#pragma once#include <iostream>
#include <cstdio>
#include <functional>class Task
{using func_t = std::function<int(int, int, char)>;// typedef std::function<int(int,int,char)>func_t;
public:Task(){}Task(int x, int y, char op, func_t func): _x(x), _y(y), _op(op), _callback(func){}std::string operator()(){int result = _callback(_x, _y, _op);char buffer[1024];snprintf(buffer, sizeof buffer, "%d %c %d = %d", _x, _op, _y, result);return buffer;}std::string toTaskString(){char buffer[1024];snprintf(buffer, sizeof buffer, "%d %c %d = ?", _x, _op, _y);return buffer;}private:int _x;int _y;char _op;func_t _callback;
};

MainCp.cc:

#include "BlockQueue.hpp"
#include "Task.hpp"
#include <ctime>
#include <sys/types.h>
#include <unistd.h>const std::string oper = "+-*/%";int mymath(int x, int y, char op)
{int result = 0;switch (op){case '+':result = x + y;break;case '-':result = x - y;break;case '*':result = x * y;break;case '/':{if (y == 0){std::cerr << "div zero error!" << std::endl;result = -1;}else{result = x / y;}}break;case '%':{if (y == 0){std::cerr << "mod zero error!" << std::endl;result = -1;}else{result = x % y;}}break;}return result;
}void *consumer(void *bq_)
{BlockQueue<Task> *bq = static_cast<BlockQueue<Task> *>(bq_);while (true){// 消费活动Task t;bq->pop(&t);std::cout << "消费任务:" << t() << std::endl;//sleep(1);}return nullptr;
}
void *productor(void *bq_)
{BlockQueue<Task> *bq = static_cast<BlockQueue<Task> *>(bq_);while (true){// 生产活动int x = rand() % 100 + 1; // 这里我们先用一个随机数构建一个数据。int y = rand() % 10;int operCode = rand() % oper.size();Task t(x, y, oper[operCode], mymath);bq->push(t);std::cout << "生产任务:" << t.toTaskString() << std::endl;sleep(1);}return nullptr;
}int main()
{srand((unsigned long)time(nullptr) ^ getpid());BlockQueue<Task> *bq = new BlockQueue<Task>();pthread_t c, p;// 生产消费要看到同一份资源,也就是阻塞队列pthread_create(&c, nullptr, consumer, bq);pthread_create(&p, nullptr, productor, bq);pthread_join(c, nullptr);pthread_join(p, nullptr);delete bq;return 0;
}

让生产者sleep1秒,看到的结果就是生产一个任务,消费一个任务。

让消费者sleep1秒,看到的结果就是消费一个任务,生产一个任务。

这样,我们就完成了一个线程给另一个线程派发任务:生产者给消费者派发任务。

  • 上面是单生产者,单消费者,如果我们直接改成多个生产者多个消费者可以吗?

MainCp.cc:

//
//...
//...
int main()
{srand((unsigned long)time(nullptr) ^ getpid());BlockQueue<Task> *bq = new BlockQueue<Task>();pthread_t c, c1, p, p1, p2;// 生产消费要看到同一份资源,也就是阻塞队列pthread_create(&p, nullptr, productor, bq);pthread_create(&p1, nullptr, productor, bq);pthread_create(&p2, nullptr, productor, bq);pthread_create(&c1, nullptr, consumer, bq);pthread_create(&c, nullptr, consumer, bq);pthread_join(c, nullptr);pthread_join(c1, nullptr);pthread_join(p, nullptr);pthread_join(p1, nullptr);pthread_join(p2, nullptr);delete bq;return 0;
}

可以看到是可以的。无论外部的线程再多,真正进入到阻塞队列里生产或消费的线程永远只有一个。

生产者要向blockqueue里放任务,消费者要向blockqueue里取任务。由于有锁的存在,这个(生产过程和消费过程)过程是串行的,也就是blockqueue里任何时刻只有一个执行流。那么:

  • 那么生产者消费者模型高效在哪里呢?创建多线程生产和消费的意义是什么呢?

1、对于生产者而言,它获取数据构建任务,是需要花时间的。

  • 如果这个任务的构建非常耗时,这个线程(生产者)在构建任务的同时,其他线程(生产者)可以并发的继续构建任务。

2、对于消费者而言,它拿到任务以后,是需要花时间处理这个任务的!

  • 如果这个任务的处理非常耗时,这个线程(消费者)在处理任务的同时,其他线程(消费者)可以并发的继续从阻塞队列里拿任务处理。

所以,高效并不体现在生产者把任务放进阻塞队列里高效,或者消费者从阻塞队列里拿任务高效。而是,体现在多个线程可以同时并发的构建或处理任务。

对于单生产单消费,它的并发性体现在,消费者从阻塞队列里拿任务和生产者构建任务,或者生产者往阻塞队列里放任务和消费者处理任务的过程是并发的。

总结:生产消费模型高效体现在,可以在生产前,和消费之后,让线程并行执行。

创建多线程生产和消费的意义:

多个线程可以并发生产,并发消费。

以上就是线程同步和基于阻塞队列的生产者消费者模型。

相关文章:

【Linux】多线程 -> 线程同步与基于BlockingQueue的生产者消费者模型

线程同步 条件变量 当一个线程互斥地访问某个变量时&#xff0c;它可能发现在其它线程改变状态之前&#xff0c;它什么也做不了。 例如&#xff1a;一个线程访问队列时&#xff0c;发现队列为空&#xff0c;它只能等待&#xff0c;直到其它线程将一个节点添加到队列中。这…...

Docker Mysql 数据迁移

查看启动命令目录映射 查看容器名称 docker ps查看容器的启动命令 docker inspect mysql8.0 |grep CreateCommand -A 20如下图所示:我这边是把/var/lib/mysql 目录映射到我宿主机的/mnt/mysql/data目录下,而且我的数量比较大使用方法1的话时间比较久,所以我采用方法2 如果没…...

四步彻底卸载IDEA!!!

各位看官早安午安晚安呀 如果您觉得这篇文章对您有帮助的话 欢迎您一键三连&#xff0c;小编尽全力做到更好 欢迎您分享给更多人哦 大家好&#xff0c;我们今天来学习四步彻底卸载IDEA&#xff01;&#xff01;&#xff01; 首先我要提醒各位 如果你想删除 IDEA 相关&#xf…...

HTTP实验(ENSP模拟器实现)

HTTP概述 HTTP&#xff08;HyperText Transfer Protocol&#xff0c;超文本传输协议&#xff09;&#xff0c;设计HTTP最初的目的是为了提供一种发布和接收HTML页面的方法。 HTTP定义了多种请求方法&#xff0c;常用的包括&#xff1a; GET&#xff1a;请求资源。 POST&…...

【网络安全】常见的web攻击

1、SQL注入攻击 定义&#xff1a; 攻击者在HTTP请求中注入恶意的SQL代码&#xff0c;当服务器利用参数构建SQL语句的时候&#xff0c;恶意的SQL代码被一起构建,并在数据库中执行。 示例&#xff1a; 用户登录&#xff1a; 输入用户名xx&#xff0c; 密码 or 1 …...

登录-05.JWT令牌-介绍

一.JWT令牌 JWT令牌是一种简洁的、自包含的格式&#xff0c;用于在通讯双方之间以json数据格式安全的传输数据。说白了&#xff0c;JWT令牌就是将json格式的数据进行封装&#xff0c;从而实现安全传输。 所谓简洁&#xff0c;就是指JWT令牌就是一个简单的字符串。 所谓自包含…...

K8S下redis哨兵集群使用secret隐藏configmap内明文密码方案详解

#作者&#xff1a;朱雷 文章目录 一、背景环境及方案说明1.1、环境说明1.2、方案一&#xff1a;使用配置文件设置密码1.3、方案二&#xff1a;使用args 的命令行传参设置密码 二、redis secret configmap deployment参考2.1 创建secret-redis.yaml参考2.2 修改configmap配置参…...

Spring框架基本使用(Maven详解)

前言&#xff1a; 当我们创建项目的时候&#xff0c;第一步少不了搭建环境的相关准备工作。 那么如果想让我们的项目做起来方便快捷&#xff0c;应该引入更多的管理工具&#xff0c;帮我们管理。 Maven的出现帮我们大大解决了管理的难题&#xff01;&#xff01; Maven&#xf…...

spring boot知识点4

1.如何监视所有spring boot微服务 安装actuator插件&#xff0c;然后通过接口查询 /actuator/health 2.spring boot项目性能如何优化 a.优化启动时间&#xff0c;去除重复的依赖 b.JVM优化&#xff08;java虚拟机优化&#xff09;&#xff0c;限制堆的最小最大值 c.数据库…...

简识Spring创建Bean方式和设计模式

一、理论解释&#xff1a; Spring在创建Bean时主要有四种方式&#xff0c;这些方式分别涉及到了不同的设计模式。以下是具体的创建方式及对应的设计模式&#xff1a; 通过反射调用构造方法创建Bean&#xff1a; 方式&#xff1a;在Spring的配置文件中&#xff0c;使用<bean…...

归并排序 Listnode* vector<int> vector<ListNode*>

加粗样式 ListNode* merge(ListNode* l1,ListNode* l2){ListNode* dummyheadnew ListNode(0);ListNode* curdummyhead;while(l1&&l2){if(l1->val>l2->val){cur->nextl2;l2l2->next;curcur->next;}else if(l1->val<l2->val){cur->nextl1…...

深度解析:大模型在多显卡服务器下的通信机制与分布式训练——以DeepSeek、Ollama和vLLM为例

一、引言&#xff1a;大模型与多显卡的必然结合 随着大模型参数规模突破千亿级&#xff08;如GPT-4、DeepSeek&#xff09;&#xff0c;单显卡的显存容量与算力已无法满足需求。多显卡并行计算成为训练与推理的核心技术&#xff0c;其核心挑战在于高效通信与负载均衡。本文以国…...

鸿蒙NEXT应用App测试-专项测试(DevEco Testing)

注意&#xff1a;大家记得先学通用测试在学专项测试 鸿蒙NEXT应用App测试-通用测试-CSDN博客 注意&#xff1a;博主有个鸿蒙专栏&#xff0c;里面从上到下有关于鸿蒙next的教学文档&#xff0c;大家感兴趣可以学习下 如果大家觉得博主文章写的好的话&#xff0c;可以点下关注…...

达梦数据库学习笔记@1

目录 达梦数据库学习笔记一、表空间管理&#xff08;一&#xff09;默认表空间&#xff08;二&#xff09;相关数据字典&#xff08;三&#xff09;表空间操作&#xff08;四&#xff09;临时表空间管理 二、重做日志管理&#xff08;一&#xff09;系统视图&#xff08;二&…...

设计模式| 观察者模式 Observer Pattern详解

目录 一、概述1.1 动机1.2 核心思想1.3 别名 二、角色与实现原理2.1 角色2.2 实现原理2.3 类图 三、经典接口实现3.1 示例3.1.1 观察者接口3.1.2 目标接口3.1.3 具体被观察者3.1.4 具体观察者3.1.5 Client3.1.6 UML时序图 3.2 特点 四、其他实现方式4.1 委托与事件&#xff08;…...

时间转换(acwing)c/c++/java/python

读取一个整数值&#xff0c;它是工厂中某个事件的持续时间&#xff08;以秒为单位&#xff09;&#xff0c;请你将其转换为小时&#xff1a;分钟&#xff1a;秒来表示。 输入格式 输入一个整数 NN。 输出格式 输出转换后的时间表示&#xff0c;格式为 hours:minutes:second…...

Rocky8 源码安装 HAProxy

HAProxy 是一款开源的高性能 负载均衡器 和 反向代理 软件&#xff0c;专注于处理高并发流量分发&#xff0c;广泛应用于企业级架构中提升服务的可用性、扩展性和安全性。 一、HAProxy 简介 1.1.HAProxy 是什么&#xff1f; 本质&#xff1a; 基于 C 语言开发 的轻量级工具&a…...

通过AI辅助生成PPT (by quqi99)

作者&#xff1a;张华 发表于&#xff1a;2025-02-23 版权声明&#xff1a;可以任意转载&#xff0c;转载时请务必以超链接形式标明文章原始出处和作者信息及本版权声明(http://blog.csdn.net/quqi99) 问题 媳妇需要将一个pdf文件中的某些部分做成PPT课件&#xff0c;我在想是…...

【从0做项目】Java文档搜索引擎(9)烧脑终章!

阿华代码&#xff0c;不是逆风&#xff0c;就是我疯 你们的点赞收藏是我前进最大的动力&#xff01;&#xff01; 希望本文内容能够帮助到你&#xff01;&#xff01; 目录 文章导读 零&#xff1a;项目结果展示 一&#xff1a;导入 二&#xff1a;问题引入 1&#xff1a;情…...

什么是 Cloud Studio DeepSeek ; 怎么实现Open WebUI快速体验

什么是 Cloud Studio DeepSeek ;怎么实现Open WebUI快速体验 一、概述 欢迎使用 Cloud Studio DeepSeek 工作空间!我们已为您预装并启动了以下服务,等待加载十几秒即可查看效果: Ollama 服务:支持通过 API 调用 DeepSeek 模型。 AnythingLLM 前端服务:提供交互式聊天界…...

rtconfig.cpython-313.pyc 在 .gitignore文件中写入 *.pyc 文件仍然没有被忽略?

在 .gitignore 文件中添加 *.pyc 和 *.*.pyc 规则时&#xff0c;如果 .pyc 文件仍然没有被忽略&#xff0c;可能有以下几种原因&#xff1a; 1. 已经被 Git 跟踪的文件 即使您在 .gitignore 中指定了忽略 .pyc 文件&#xff0c;Git 仍然会跟踪已经被提交到版本库中的文件。如…...

Linux 第二次脚本作业

1、需求&#xff1a;判断192.168.1.0/24网络中&#xff0c;当前在线的ip有哪些&#xff0c;并编写脚本打印出来。 2、设计一个 Shell 程序&#xff0c;在/userdata 目录下建立50个目录&#xff0c;即 user1~user50&#xff0c;并设置每个目录的权限&#xff0c;其中其他用户的权…...

mysql的源码包安装

安装方式一&#xff1a;&#xff08;编译好的直接安装&#xff09; 1.添加一块10G的硬盘&#xff0c;给root逻辑卷扩容 &#xff08;下面安装方式二有&#xff0c;一模一样的装就行&#xff0c;我就不写了&#xff0c;再写的话篇幅就太长了&#xff09; 2.下载编译好的源码包…...

《论面向对象的建模及应用》审题技巧 - 系统架构设计师

论面向对象的建模及应用写作框架 一、考点概述 本论题“论面向对象的建模及应用”主要考察软件测试工程师对面向对象建模技术的理解和应用能力。具体涵盖以下几个方面&#xff1a; 面向对象建模的基本概念 &#xff1a;这包括理解面向对象编程&#xff08;OOP&#xff09;的基…...

#渗透测试#批量漏洞挖掘#畅捷通T+远程命令执行漏洞

免责声明 本教程仅为合法的教学目的而准备,严禁用于任何形式的违法犯罪活动及其他商业行为,在使用本教程前,您应确保该行为符合当地的法律法规,继续阅读即表示您需自行承担所有操作的后果,如有异议,请立即停止本文章读。 目录 一、漏洞概况 二、攻击特征 三、应急处置…...

Sky Hackathon 清水湾的水 AI美食助手

这里写自定义目录标题 视频 视频 video...

【2024 CSDN博客之星】大学四年,我如何在CSDN实现学业与事业的“双逆袭”?

前言&#xff1a; Hello大家好&#xff0c;我是Dream。不知不觉2024年已经过去&#xff0c;自己也马上迈入23岁&#xff0c;感慨时间飞快&#xff0c;从19岁刚入大学加入CSDN&#xff0c;到现在大学毕业已经整整四年了。CSDN陪伴我走过了最青涩的四年大学时光&#xff0c;在这里…...

《AI赋能星际探索:机器人如何开启宇宙新征程!》

在人类对宇宙无尽的探索中&#xff0c;空间探索任务始终充满挑战。从遥远星球的探测&#xff0c;到空间站的维护&#xff0c;每一项任务都需要高精度、高可靠性的操作。人工智能&#xff08;AI&#xff09;的迅猛发展&#xff0c;为空间探索机器人带来了革命性的变革&#xff0…...

06排序 + 查找(D1_排序(D1_基础学习))

目录 学习预热&#xff1a;基础知识 一、什么是排序 二、为什么要排序 三、排序的稳定性 四、排序稳定性的意义 五、排序分类方式 方式一&#xff1a;内外分类 方式二&#xff1a;比较分类 六、排序算法性能评估 1. 算法的时间复杂度 2. 算法的空间复杂度 七、知识小…...

【数据挖掘】深度挖掘

【数据挖掘】深度挖掘 目录&#xff1a;1. 减少样本集的数量知识点示例 2. 对噪声比集剪枝知识点示例建立局部树代码示例&#xff08;使用 Python 和 scikit - learn 库构建局部决策树&#xff09;代码解释注意事项 最大超平面定义原理求解方法代码示例&#xff08;使用 Python…...