【Linux初阶】多线程3 | 线程同步,生产消费者模型(普通版、BlockingQueue版)
文章目录
- ☀️一、线程同步
- 🌻1.条件变量
- 🌻2.同步概念与竞态条件
- 🌻3.条件变量函数
- 🌻4.条件变量使用规范
- 🌻5.代码案例
- ☀️二、生产者消费者模型
- 🌻1.为何要使用生产者消费者模型
- 🌻2.生产者消费者模型优点
- 🌻3.生产消费的关系
- ☀️三、基于BlockingQueue的生产者消费者模型
- 🌻1.概念
- 🌻2.代码示例
- ⚡(1)生产者给消费者派发整形数据(简单版)
- ⚡(2)生产者给消费者派发任务(复杂版)
- 🌻3.探究:生产消费模型高校在哪里
☀️一、线程同步
🌻1.条件变量
- 当一个线程互斥地访问某个变量时,它可能发现在其它线程改变状态之前,它什么也做不了。
- 例如一个线程访问队列时,发现队列为空,它只能等待,只到其它线程将一个节点添加到队列中。这种情况就需要用到条件变量。
- 通过条件变量,我们可以实现线程同步,即可让线程顺序进行。
- 我们访问临界资源的模式一般是这样的:对临界资源加锁,判断(是否满足条件变量/生产消费条件),解锁。
🌻2.同步概念与竞态条件
同步
:在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问题,叫做同步。竞态条件
:因为线程运行的时序问题,而导致程序异常,我们称之为竞态条件。
🌻3.条件变量函数
- (1)初始化
动态设置
int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict
attr);
参数:cond:要初始化的条件变量attr:NULL
静态设置
pthread_cond_t cond = PTHREAD_COND_INITIALIZER; //静态设置条件变量(不用初始化、销毁)
- (2)销毁
int pthread_cond_destroy(pthread_cond_t *cond)
- (3)等待条件满足
int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);
参数:cond:要在这个条件变量上等待mutex:互斥量,后面详细解释
-
在新线程内部调用
pthread_cond_wait
,可让线程加入等待队列中。 -
(4)唤醒等待
int pthread_cond_signal(pthread_cond_t *cond);
int pthread_cond_broadcast(pthread_cond_t *cond);
- 在新线程内部调用
pthread_cond_signal
,就是把线程从等待队列中拿出来,放到CPU中运行。
🌻4.条件变量使用规范
- 等待条件代码
pthread_mutex_lock(&mutex);while (条件为假)pthread_cond_wait(cond, mutex);修改条件pthread_mutex_unlock(&mutex);
- 给条件发送信号代码
pthread_mutex_lock(&mutex);设置条件为真pthread_cond_signal(cond);pthread_mutex_unlock(&mutex);
🌻5.代码案例
- makefile
testCond:testCond.ccg++ -o $@ $^ -std=c++11 -lpthread
.PHONY:clean
clean:rm -f testCond
- testCond.cc
#include <iostream>
#include <string>
#include <unistd.h>
#include <pthread.h>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); //将新线程放入等待队列// wait函数参数为什么要有mutex?为了后续释放和再次获取mutex(锁)//线程进入阻塞队列时要释放锁,为了能让别的线程能访问该临界资源//线程被唤醒之后需要重新把锁申请回来//判断暂时省略std::cout << name << " -> " << tickets << std::endl;tickets--;pthread_mutex_unlock(&mutex);}
}int main()
{// 通过条件变量控制线程的执行pthread_t t[5];for (int i = 0; i < 5; i++){char* name = new char[64];snprintf(name, 64, "thread %d", i + 1);pthread_create(t + i, nullptr, start_routine, name);}while (true){sleep(1); //#include <unistd.h>// pthread_cond_signal(&cond); //每次循环唤醒一个新线程pthread_cond_broadcast(&cond); //唤醒一批线程(所有线程都会被唤醒)std::cout << "main thread wakeup one thread..." << std::endl;}for (int i = 0; i < 5; i++){pthread_join(t[i], nullptr);}return 0;
}
- 运行结果
☀️二、生产者消费者模型
🌻1.为何要使用生产者消费者模型
- 生产者消费者模式就是通过一个容器来解决生产者和消费者的强耦合问题。
- 生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。
- 这个阻塞队列就是用来给生产者和消费者解耦的。
- 阻塞队列(临时保存的数据场所)在缓冲区中。
🌻2.生产者消费者模型优点
- 解耦(将生产和消费过程进行分离)
- 支持并发
- 支持忙闲不均
🌻3.生产消费的关系
- 生产者和生产者之间 - 互斥关系(竞争关系)。
- 消费者和消费者之间 - 互斥关系。
- 生产者和消费者之间 - 互斥 && 同步(生产消费需要访问同一份资源时为互斥,生产消费协同进行为同步)。
生产消费模型巧记 - 321原则:
- 3种关系:生产者和生产者(互斥),消费者和消费者(互斥),生产者和消费者(互斥(保证共享资源安全性) && 同步) 。
- 2种角色:生产者线程,消费者线程。
- 1个交易场所:一段特定结构的缓冲区。
- 生产消费的产品就是数据。
☀️三、基于BlockingQueue的生产者消费者模型
🌻1.概念
- 在多线程编程中
阻塞队列(Blocking Queue)
是一种常用于实现生产者和消费者模型的数据结构。 - 其与普通的队列区别在于,当队列为空时,从队列获取元素的操作将会被阻塞,直到队列中被放入了元素;
- 当队列满时,往队列里存放元素的操作也会被阻塞,直到有元素被从队列中取出(以上的操作都是基于不同的线程来说的,线程在对阻塞队列进程操作时会被阻塞)。
🌻2.代码示例
⚡(1)生产者给消费者派发整形数据(简单版)
#include <iostream>
#include <queue>
#include <stdlib.h>
#include <pthread.h>#define NUM 8class BlockQueue {
private:std::queue<int> q;int cap;pthread_mutex_t lock;pthread_cond_t full;pthread_cond_t empty;private:void LockQueue(){pthread_mutex_lock(&lock);}void UnLockQueue(){pthread_mutex_unlock(&lock);}void ProductWait(){pthread_cond_wait(&full, &lock);}void ConsumeWait(){pthread_cond_wait(&empty, &lock);}void NotifyProduct(){pthread_cond_signal(&full);}void NotifyConsume(){pthread_cond_signal(&empty);}bool IsEmpty(){return (q.size() == 0 ? true : false);}bool IsFull(){return (q.size() == cap ? true : false);}public:BlockQueue(int _cap = NUM) :cap(_cap){pthread_mutex_init(&lock, NULL);pthread_cond_init(&full, NULL);pthread_cond_init(&empty, NULL);}void PushData(const int& data){LockQueue();while (IsFull()) {NotifyConsume();std::cout << "queue full, notify consume data, product stop." << std::endl;ProductWait();}q.push(data);// NotifyConsume();UnLockQueue();}void PopData(int& data){LockQueue();while (IsEmpty()) {NotifyProduct();std::cout << "queue empty, notify product data, consume stop." << std::endl;ConsumeWait();}data = q.front();q.pop();// NotifyProduct();UnLockQueue();}~BlockQueue(){pthread_mutex_destroy(&lock);pthread_cond_destroy(&full);pthread_cond_destroy(&empty);}
};void* consumer(void* arg)
{BlockQueue* bqp = (BlockQueue*)arg;int data;for (; ; ) {bqp->PopData(data);std::cout << "Consume data done : " << data << std::endl;}
}//more faster
void* producter(void* arg)
{BlockQueue* bqp = (BlockQueue*)arg;srand((unsigned long)time(NULL));for (; ; ) {int data = rand() % 1024;bqp->PushData(data);std::cout << "Prodoct data done: " << data << std::endl;// sleep(1);}
}int main()
{BlockQueue bq;pthread_t c, p;pthread_create(&c, NULL, consumer, (void*)&bq);pthread_create(&p, NULL, producter, (void*)&bq);pthread_join(c, NULL);pthread_join(p, NULL);return 0;
}
⚡(2)生产者给消费者派发任务(复杂版)
生产者派发任务(计算任务) -> 放入阻塞队列1 -> 消费处理任务 -> 放入阻塞队列2(将结果储存) -> 记录任务结果(保存在文件中)
- BlockQueue.hpp
#include <iostream>
#include <queue>
#include <pthread.h>const int gmaxcap = 500;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. 判断// 细节2: 充当条件判断的语法必须是while,不能用ifwhile (is_full()) //bug?{// 细节1: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_signal(&_ccond); // 这里可以有一定的策略}void pop(T* out) // 输出型参数:*, // 输入输出型:&{pthread_mutex_lock(&_mutex);//1. 判断while (is_empty()) //bug?{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
#include <iostream>
#include <string>
#include <cstdio>
#include <functional>class CalTask
{using func_t = std::function<int(int, int, char)>;// typedef std::function<int(int,int)> func_t;
public:CalTask(){}CalTask(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;
};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;}elseresult = x / y;}break;case '%':{if (y == 0){std::cerr << "mod zero error!" << std::endl;result = -1;}elseresult = x % y;}break;default:// do nothingbreak;}return result;
}class SaveTask
{typedef std::function<void(const std::string&)> func_t;
public:SaveTask(){}SaveTask(const std::string& message, func_t func): _message(message), _func(func){}void operator()(){_func(_message);}
private:std::string _message;func_t _func;
};void Save(const std::string& message)
{const std::string target = "./log.txt";FILE* fp = fopen(target.c_str(), "a+");if (!fp){std::cerr << "fopen error" << std::endl;return;}fputs(message.c_str(), fp);fputs("\n", fp);fclose(fp);
}
- MainCp.cc
#include "BlockQueue.hpp"
#include "Task.hpp"
#include <sys/types.h>
#include <unistd.h>
#include <ctime>//C:计算
//S: 存储
template<class C, class S>
class BlockQueues
{
public:BlockQueue<C> *c_bq;BlockQueue<S> *s_bq;
};void *productor(void *bqs_)
{BlockQueue<CalTask> *bq = (static_cast<BlockQueues<CalTask, SaveTask> *>(bqs_))->c_bq;// BlockQueue<Task> *bq = static_cast<BlockQueue<Task> *>(bq_);while (true){// sleep(3);// 生产活动,从数据库?从网络,从外设??拿来的用户数据!!int x = rand() % 100 + 1; // 在这里我们先用随机数,构建一个数据int y = rand() % 10;int operCode = rand() % oper.size();CalTask t(x, y, oper[operCode], mymath);bq->push(t);std::cout << "productor thread, 生产计算任务: " << t.toTaskString() << std::endl;}return nullptr;
}void *consumer(void *bqs_)
{BlockQueue<CalTask> *bq = (static_cast<BlockQueues<CalTask, SaveTask> *>(bqs_))->c_bq;BlockQueue<SaveTask> *save_bq = (static_cast<BlockQueues<CalTask, SaveTask> *>(bqs_))->s_bq;while (true){// 消费活动CalTask t;bq->pop(&t);std::string result = t(); // 任务非常耗时!!std::cout << "cal thread,完成计算任务: " << result << " ... done"<< std::endl;// SaveTask save(result, Save);// save_bq->push(save);// std::cout << "cal thread,推送存储任务完成..." << std::endl; //sleep(1);}return nullptr;
}void *saver(void *bqs_)
{BlockQueue<SaveTask> *save_bq = (static_cast<BlockQueues<CalTask, SaveTask> *>(bqs_))->s_bq;while(true){SaveTask t;save_bq->pop(&t);t();std::cout << "save thread,保存任务完成..." << std::endl; }return nullptr;
}int main()
{srand((unsigned long)time(nullptr) ^ getpid());BlockQueues<CalTask, SaveTask> bqs;bqs.c_bq = new BlockQueue<CalTask>();bqs.s_bq = new BlockQueue<SaveTask>();pthread_t c[2], p[3], s;pthread_create(p, nullptr, productor, &bqs);pthread_create(p+1, nullptr, productor, &bqs);pthread_create(p+2, nullptr, productor, &bqs);pthread_create(c, nullptr, consumer, &bqs);pthread_create(c+1, nullptr, consumer, &bqs);pthread_create(&s, nullptr, saver, &bqs); //saver - 保存在文件pthread_join(c[0], nullptr);pthread_join(c[1], nullptr);pthread_join(p[0], nullptr);pthread_join(p[1], nullptr);pthread_join(p[2], nullptr);pthread_join(s, nullptr); //delete bqs.c_bq;delete bqs.s_bq;return 0;
}
- makefile
MainCp:MainCp.ccg++ -o $@ $^ -std=c++11 -lpthread
.PHONY:clean
clean:rm -f MainCp
- 运行结果
🌻3.探究:生产消费模型高校在哪里
- 首先我们要清楚,生产放入队列、消费拿出队列的动作是原子的。
- 对于生产端,我们构建一个任务可能十分耗时间,构建完成之后,可以竞争式的放进队列。简单来说,就是每个线程之间的任务构造相互独立,不需要一个一个任务串行构造,可以并发式构造,只有放进队列时要一个一个放进去,节省了构造任务的时间。
- 对于消费端,只有将任务取出队列时要一个一个取,而任务(算法等)可以并发式的实现,节省了任务实现的时间。
- 总结:可以在生产之前,和消费之后,让线程并行执行。
🌹🌹 多线3 的知识大概就讲到这里啦,博主后续会继续更新更多C++ 和 Linux的相关知识,干货满满,如果觉得博主写的还不错的话,希望各位小伙伴不要吝啬手中的三连哦!你们的支持是博主坚持创作的动力!💪💪
相关文章:

【Linux初阶】多线程3 | 线程同步,生产消费者模型(普通版、BlockingQueue版)
文章目录 ☀️一、线程同步🌻1.条件变量🌻2.同步概念与竞态条件🌻3.条件变量函数🌻4.条件变量使用规范🌻5.代码案例 ☀️二、生产者消费者模型🌻1.为何要使用生产者消费者模型🌻2.生产者消费者模…...

JUC并发编程——四大函数式接口(基于狂神说的学习笔记)
四大函数式接口 函数式接口:只有一个方法的接口 ,例如:Runnable接口 Function 函数型接口,有一个输入参数,有一个输出 源码: /*** Represents a function that accepts one argument and produces a resul…...

【2】c++11新特性(稳定性和兼容性)—>超长整型 long long
c11标准要求long long整型可以在不同的平台上有不同的长度,但是至少64位,long long整型有两种: 有符号long long:–对应类型的数值可以使用LL或者ll后缀 long long num1 123456789LL; long long num2 123456789ll;无符号unsign…...

AI算法检测对无人军用车辆的MitM攻击
南澳大利亚大学和查尔斯特大学的教授开发了一种算法来检测和拦截对无人军事机器人的中间人(MitM)攻击。 MitM 攻击是一种网络攻击,其中两方(在本例中为机器人及其合法控制器)之间的数据流量被拦截,以窃听或…...

运维 | 如何在 Linux 系统中删除软链接 | Linux
运维 | 如何在 Linux 系统中删除软链接 | Linux 介绍 在 Linux 中,符号链接(symbolic link,或者symlink)也称为软链接,是一种特殊类型的文件,用作指向另一个文件的快捷方式。 使用方法 我们可以使用 ln…...

Jmeter接口测试:jmeter导入和导出接口的处理
JMeter测试导入接口 利用Jmeter测试上传文件,首先可根据接口文档或者fiddler抓包分析文件上传的接口;如下图: 以下是我通过fiddler所截取的文件上传的接口 1、填写导入接口的信息 查看文件上传栏下的填写信息: 文件名称&#x…...

一文了解 Go fmt 标准库的常用占位符及其简单使用
今天分享的内容是 Go fmt 标准库的常用占位符及其简单使用。如果本文对你有帮助,不妨点个赞,如果你是 Go 语言初学者,不妨点个关注,一起成长一起进步,如果本文有错误的地方,欢迎指出 占位符 通过占位符&a…...

Linux命令(94)之history
linux命令之history 1.history介绍 linux命令history会记录并显示用户所执行过的所有命令,也可以对其命令进行修改和删除操作。 2.history用法 history [参数] history参数 参数说明-a将当前会话的历史信息追加到历史文件(.bash_history)中-c删除所有条目从而清…...

Prompt 驱动架构设计:探索复杂 AIGC 应用的设计之道?
你是否曾经想过,当你在 Intellij IDEA 中输入一个段代码时,GitHub 是如何给你返回相关的结果的?其实,这背后的秘密就是围绕 Prompt 生成而构建的架构设计。 Prompt 是一个输入的文本段落或短语,用于引导 AI 生成模型执…...

【代码随想录】算法训练营 第三天 第二章 链表 Part 1
目录 链表基础 链表的定义 203. 移除链表元素 题目 思路 代码 直接删除法 虚拟头结点辅助法 707. 设计链表 题目 思路 代码 206. 反转链表 题目 思路 代码 双指针法 递归法 链表基础 链表是一种通过指针串在一起的线性结构,每个节点都由数据域和指…...

winform开发经验(1)——调用Invoke更新UI时程序卡死原因以及解决办法
1、问题代码如下: private void Form1_Load(object sender, EventArgs e){this.Invoke(new Action(()...

JNI 的数据类型以及和Java层之间的数据转换
JNI的数据类型和类型签名 数据类型 JNI的数据类型包含两种:基本类型和引用类型。 基本类型主要有jboolean、jchar、jint等,它们和Java中的数据类型的对应关系如下表所示。 JNI中的引用类型主要有类、对象和数组,它们和Java中的引用类型的对…...

EFLK与logstash过滤
目录 一、Filebeat工作原理: 二、为什么要使用Filebeat: 三、Filebeat和Logstash的区别: 四、logstash 的过滤插件: 五、FilebeatELK 部署: 1. 安装filebeat: 2. 设置 filebeat 的主配置文件࿱…...

docker jenkins
mkdir jenkins_home chown -R 1000:1000 /root/jenkins_home/docker run -d --name myjenkins -v /root/jenkins_home:/var/jenkins_home -p 8080:8080 -p 50000:50000 --restarton-failure jenkins/jenkins:lts-jdk17参考 Official Jenkins Docker imageDocker 搭建 Jenkins …...

单例模式之「双重校验锁」
单例模式之「双重校验锁」 单例模式 单例即单实例,只实例出来一个对象。一般在创建一些管理器类、工具类的时候,需要用到单例模式,比如JDBCUtil 类,我们只需要一个实例即可(多个实例也可以实现功能,但是增…...

2023年中国商业版服务器操作系统市场发展规模分析:未来将保持稳定增长[图]
服务器操作系统一般指的是安装在大型计算机上的操作系统,比如Web服务器、应用服务器和数据库服务器等,是企业IT系统的基础架构平台,也是按应用领域划分的三类操作系统之一。同时服务器操作系统也可以安装在个人电脑上。 服务器操作系统分类 …...

BIM如何通过3D开发工具HOOPS实现WEB轻量化?
随着建筑行业的数字化转型和信息建模技术的不断发展,建筑信息模型(BIM)已经成为设计、建造和管理建筑项目的标准。然而,BIM模型通常包含大量的数据,导致在Web上的传输和查看效率低下。为了解决这一挑战,HOO…...

Unity 3D基础——通过四元数控制对象旋转
在这个例子中,通过键盘的左右方向来控制场景中的球体 Sphere 的横向运动,而 Cube 立方体则会一直朝着球体旋转。 1.在场景中新建一个 Cube 立方体和一个 Sphere 球体,在 Inspector 视图中设置 Cube 立方体的坐标为(3,0…...

python--短路运算,把0、空字符串和None看成 False,其他数值和非空字符串都看成 True
代码 print(3 and 4 and 5) # 5 print(5 and 6 or 7) # 6 4 > 3 and print(‘hello world’) # 输出hello world 注释: 在逻辑运算中,不一定逻辑运算符的两边都是纯表达式。也可以是数值类型的数据。 Python把0、空字符串和None看成 Falseÿ…...

《算法通关村第一关——链表青铜挑战笔记》
《算法通关村第一关——链表青铜挑战笔记》 Java如何构造出链表 概念 如何构造出链表,首先必须了解什么是链表! 单向链表就像一个铁链一样,元素之间相互链接,包含多个节点,每个节点有一个指向后继元素的next指针。…...

【深度学习实验】循环神经网络(四):基于 LSTM 的语言模型训练
目录 一、实验介绍 二、实验环境 1. 配置虚拟环境 2. 库版本介绍 三、实验内容 0. 导入必要的工具包 1. RNN与梯度裁剪 2. LSTM模型 3. 训练函数 a. train_epoch b. train 4. 文本预测 5. GPU判断函数 6. 训练与测试 7. 代码整合 经验是智慧之父,记忆…...

IOS课程笔记[1-3] 第一个IOS应用
安装开发环境 安装Xcode软件 历史版本查找 https://developer.apple.com/download/all/?qdebug 创建Object-C项目 启动过程 步骤 1.加载Main中定义的storyBoard 2.加载Main控制器 3.加载控制器下的View组件显示 获取控件的两种方式 定义属性连线:property (…...

Flink的基于两阶段提交协议的事务数据汇实现
背景 在flink中可以通过使用事务性数据汇实现精准一次的保证,本文基于Kakfa的事务处理来看一下在Flink 内部如何实现基于两阶段提交协议的事务性数据汇. flink kafka事务性数据汇的实现 1。首先在开始进行快照的时候也就是收到checkpoint通知的时候,在…...

树模型(三)决策树
决策树是什么?决策树(decision tree)是一种基本的分类与回归方法。 长方形代表判断模块 (decision block),椭圆形成代表终止模块(terminating block),表示已经得出结论,可以终止运行。从判断模块引出的左右箭头称作为分支(branch)…...

vueday01——使用属性绑定+ref属性定位获取id
1.属性绑定(Attribute 绑定) 第一种写法 <div v-bind:id"refValue"> content </div> 第二种写法(省略掉v-bind) <div :id"refValue"> content </div> 2.代码展示 <template…...

LeetCode 260. 只出现一次的数字 III:异或
【LetMeFly】260.只出现一次的数字 III 力扣题目链接:https://leetcode.cn/problems/single-number-iii/ 给你一个整数数组 nums,其中恰好有两个元素只出现一次,其余所有元素均出现两次。 找出只出现一次的那两个元素。你可以按 任意顺序 返…...

使用PyTorch解决多分类问题:构建、训练和评估深度学习模型
💗💗💗欢迎来到我的博客,你将找到有关如何使用技术解决问题的文章,也会找到某个技术的学习路线。无论你是何种职业,我都希望我的博客对你有所帮助。最后不要忘记订阅我的博客以获取最新文章,也欢…...

基于nodejs+vue网课学习平台
各功能简要描述如下: 1个人信息管理:包括对学生用户、老师和管理员的信息进行录入、修改,以及老师信息的审核等 2在库课程查询:用于学生用户查询相关课程的功能 3在库老师查询:用于学生用户查询相关老师教学的所有课程的功能。 4在库学校查询:用于学生用户查询相关学…...

读书笔记:Effective C++ 2.0 版,条款13(初始化顺序==声明顺序)、条款14(基类有虚析构)
条款13: 初始化列表中成员列出的顺序和它们在类中声明的顺序相同 类成员是按照它们在类里被声明的顺序进行初始化的,和它们在成员初始化列表中列出的顺序没一点关系。 根本原因可能是考虑到内存的分布,按照定义顺序进行排列。 另外,初始化列表…...

flutter开发实战-下拉刷新与上拉加载更多实现
flutter开发实战-下拉刷新与上拉加载更多实现 在开发中经常遇到列表需要下拉刷新与上拉加载更多,这里使用EasyRefresh,版本是3.3.21 一、什么是EasyRefresh EasyRefresh可以在Flutter应用程序上轻松实现下拉刷新和上拉加载。它几乎支持所有Flutter Sc…...