【Linux】生产者消费者模型 + 线程池的介绍和代码实现

前言
上节我们学习了线程的同步与互斥,学习了互斥锁和条件变量的使用。本章我们将学习编程的一个重要模型,生产者消费者模型,并且运用之前学的线程同步和互斥的相关接口来实现阻塞队列和环形队列,最后再来实现一个简易的线程池。

目录

    • 1.生产者消费者模型
      • 1.1 基于 阻塞队列(BlockingQueue) 的生产者消费者模型
      • 1.2 基于 环形队列 的生产者消费者模型
        • 1.2.1 POSIX信号量(可用于线程同步)
        • 1.2.2 基于环形队列的生产消费模型
      • 1.3 两种实现的区别
    • 2 .线程池的实现(懒汉模式)
      • 2.1 单例模式复习:
      • 2.2 线程池成员变量:
      • 2.3 构造和析构函数
      • 2.4 线程池启动
      • 2.5 Pop和Push Task任务
      • 2.6 Task类
      • 2.7 Main.cc的实现:
    • 3. STL,智能指针和线程安全

1.生产者消费者模型

在这里插入图片描述
生产者消费者模型是同步与互斥的最典型的应用场景。

3 2 1 原则:

  • 生产者和生产者(互斥)消费者和消费者(互斥)生产者和消费者 互斥 / 同步(非常重要)): 3种关系
  • 生产者和消费者: 线程承担的2种角色
  • 超市:内存中特定的一种内存结构(数据结构): 1个交易场所

1.1 基于 阻塞队列(BlockingQueue) 的生产者消费者模型

在这里插入图片描述

设计的这个队列要保证,队列元素如果为满的情况下,就不能让生产者生产了,如果为空的情况下,就不能让消费者来消费了,那么这个的队列就称作为阻塞队列。

  • 成员变量:
  • 需要一个队列来将对象存入(队列就好比是超市)。
  • 我们还需要用于访问控制的互斥锁,在同一时刻只能有一个线程访问队列。
  • 我们需要两个用户线程同步的条件变量,因为我们需要在不同的条件下通知的线程(生产者or消费者)。

代码演示:

  #pragma once#include <iostream>
#include <queue>
#include <cstdlib>
#include <unistd.h>
#include <pthread.h>// 默认容量大小
const uint32_t gDefaultCap = 5;template <class T>
class BlockQueue
{
public:BlockQueue(uint32_t cap = gDefaultCap) : cap_(cap){pthread_mutex_init(&lock, nullptr);pthread_cond_init(&full, nullptr);pthread_cond_init(&empty, nullptr);}~BlockQueue(){pthread_mutex_destroy(&lock);pthread_cond_destroy(&full);pthread_cond_destroy(&empty);}
private:uint32_t cap_;           // 容量std::queue<T> bq_;            // blockqueuepthread_mutex_t lock;  // 保护阻塞队列的互斥锁pthread_cond_t full; // 让消费者等待的条件变量pthread_cond_t empty; // 让生产者等待的条件变量
};
  • 插入队列和出队列
 void PushData(const int &data)
{LockQueue();while(IsFull()){std::cout << "queue full, notify consume data, product stop." << std::endl;ProductWait();} q.push(data);//生产完了,就要去唤醒消费者NotifyConsume();UnLockQueue();
}

补充解释:

  • bq是否为满,程序员视角的条件
    (1)满(不生产)(2)不满(生产)
  • if(满)不生产(不仅仅要不生产),休眠(更要休眠),休眠期间消费线程就可以去申请锁了。
  • else if(不满)生产,唤醒消费者。

为什么要用while判断而不用if判断:

  • 为了防止有一些误唤醒的条件触发
  • 等待条件变量前:当我等待的时候,会自动释放mutex_(因为不能拿着锁去等)
  • 使用While条件的话,会重新判断isfull()条件是否满足,重新进入线程等待中去。

出队(pop):

 void PopData(int &data)
{LockQueue();while(IsEmpty()){std::cout << "queue empty, notify product data, consume stop." << std::endl;ConsumeWait();
}data = q.front();q.pop();NotifyProduct();UnLockQueue();
}

补充解释:

  • bq是否为空,程序员视角的条件:
    (1)空(不消费)(2)有(消费)
  • if(空)不消费,休眠。
  • else if(有)消费,唤醒生产者。

对其中一些加锁及条件变量等进行封装:

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 );
}
  • Task任务类
    我们可以给队列分配Task对象(任务)
#pragma once#include <iostream>
#include <string>using namespace std;class Task
{
public:Task() : elemOne_(0), elemTwo_(0), operator_('0'){}Task(int one, int two, char op) : elemOne_(one), elemTwo_(two), operator_(op){}int operator() (){return run();}int run(){int result = 0;switch (operator_){case '+':result = elemOne_ + elemTwo_;break;case '-':result = elemOne_ - elemTwo_;break;case '*':result = elemOne_ * elemTwo_;break;case '/':{if (elemTwo_ == 0){cout << "div zero, abort" << endl;result = -1;}else{result = elemOne_ / elemTwo_;}}break;case '%':{if (elemTwo_ == 0){std::cout << "mod zero, abort" << std::endl;result = -1;}else{result = elemOne_ % elemTwo_;}}break;default:cout << "非法操作: " << operator_ << endl;break;}return result;}// 输出型参数int get(int *e1, int *e2, char *op){*e1 = elemOne_;*e2 = elemTwo_;*op = operator_;}
private:int elemOne_;int elemTwo_;char operator_;
};
  • 生产和消费任务
  • 生产任务
void *productor(void *args)
{BlockQueue<Task> *bqp = static_cast<BlockQueue<Task> *>(args);while (true){// 1. 制作任务 --- 要不要花时间?? -- 网络,磁盘,用户int one = rand() % 50;int two = rand() % 20;char op = ops[rand() % ops.size()];Task t(one, two, op);// 2. 生产任务bqp->push(t);cout << "producter[" << pthread_self() << "] " << (unsigned long)time(nullptr) << " 生产了一个任务: " << one << op << two << "=?" << endl;sleep(1);}
}
  • 消费任务
 const std::string ops = "+-*/%";void *consumer(void *args)
{BlockQueue<Task> *bqp = static_cast<BlockQueue<Task> *>(args);while (true){Task t = bqp->pop(); // 消费任务int result = t();    // 处理任务 --- 任务也是要花时间的!int one, two;char op;t.get(&one, &two, &op);cout << "consumer[" << pthread_self() << "] " << (unsigned long)time(nullptr) << " 消费了一个任务: " << one << op << two << "=" << result << endl;}
}

main任务:

int main()
{// 生产者用来生产计算任务,消费者用来消费计算任务BlockQueue<Task> bq;pthread_t c, p;pthread_create(&c, nullptr, consumer, &bq);pthread_create(&p, nullptr, productor, &bq);pthread_join(c, nullptr);pthread_join(p, nullptr);return 0;
}

补充解释:

  • 生产者生产任务的时候和消费者消费任务的时候是并发执行的
  • 并发并不是在交易场所中并发。
  • 在消费的同时也在制作任务,并发体现就在这里。

1.2 基于 环形队列 的生产者消费者模型

生产消费模型用上了循环队列之后,就会有一个很大的优势:

  • 因为生产者和消费者访问的(假设是数组实现的循环队列)是不同下标位置
  • 这二者访问的并非同一块内存空间,所以这就实现了同时访问
  • 可以实现生产和消费过程并发。。

对比之前的阻塞队列实现:

  • 之前学的queue是整体被使用的,没法被切割。
  • 这个临界资源可以被划分成不同的区域,要用信号量将这些区域保护起来。
  • 要写一个基于固定大小的环形队列, 多线程情况下根本就不用考虑队列为满还是为空,因为信号量帮我们考虑

在这里插入图片描述

此时就相当于把循环队列这个临界资源分成了一小块一小块,只有满或空的时候,头指针和尾指针才会指向同一块数组空间,其他时间都是不冲突的!

1.2.1 POSIX信号量(可用于线程同步)

信号量本质上是一个计数器,是一个描述临界资源数量的计数器。
保证不会在限有资源的情况下让多的线程进入到临界区对临界资源的访问。通过信号量来限制进入临界资源当中的线程的个数

  • P操作:申请资源(原子的)
  • V操作:归还资源(原子的)

持有0和1的信号量叫做,二元信号量

  • 初始化一个未命名的信号量:
    在这里插入图片描述
    销毁信号量:
    在这里插入图片描述
    等待信号量
功能:等待信号量,会将信号量的值减1
int sem_wait(sem_t *sem); //P()

发布信号量

功能:发布信号量,表示资源使用完毕,可以归还资源了。将信号量值加1int sem_post(sem_t *sem);//V()
1.2.2 基于环形队列的生产消费模型
  • 环形队列采用数组模拟,用模运算来模拟环状特性
  • 在这里插入图片描述
    成员变量
#include <iostream>
#include <vector>
#include <stdlib.h>
#include <semaphore.h>
#include <pthread.h>
#define NUM 16
class RingQueue{
private:std::vector<int> q; 环形队列int cap;sem_t data_sem;  // 衡量空间计数器,productorsem_t space_sem; // 衡量数据计数器,consumerint consume_step; // 当前生产者写入的位置int product_step; // 当前消费者读取的位置,如果是多线程,cIndex_也是临界资源pthread_mutex_t _c_mutex;pthread_mutex_t _p_mutex;public:
RingQueue(int _cap = NUM):q(_cap),cap(_cap)
{
sem_init(&data_sem, 0, 0);
sem_init(&space_sem, 0, cap);
consume_step = 0;
product_step = 0;
pthread_mutex_init(&_c_mutex, nullptr);
pthread_mutex_init(&_p_mutex, nullptr);
}~
RingQueue()
{
sem_destroy(&data_sem);
sem_destroy(&space_sem);
pthread_mutex_destroy(&_c_mutex);
pthread_mutex_destroy(&_p_mutex);
}}
  • 生产和消费函数
void push(const T &in){// 1. 可以不用在临界区内部做判断,就可以知道临界资源的使用情况// 2. 什么时候用锁,什么时候用sem?你对应的临界资源,是否被整体使用!P(_space_sem);  // P() Lock(_p_mutex); //?    1// 一定有对应的空间资源给我!不用做判断,是哪一个呢?_ring[_p_step++] = in;_p_step %= _cap;Unlock(_p_mutex);V(_data_sem);}// 消费void pop(T *out){P(_data_sem);Lock(_c_mutex); //?*out = _ring[_c_step++];_c_step %= _cap;Unlock(_c_mutex);V(_space_sem);}

生产者和消费者都为空的时候,一定能保证生产线程先运行,因为一开始消费线程的数据信号量一开始为0,sem_wait(&dataSem_)函数一开始要阻塞等待
在这里插入图片描述
环形队列的使用:(重点)

  • 生产者生产时:空间多了一个,申请了一个空间(空间信号量 - 1),数据信号量 + 1。

  • 消费者消费时:空间少了一个,释放了一个空间(空间信号量 + 1),数据信号量 - 1。

  • 有信号量帮我们做了访问控制,所以我们不需要判断循环队列什么时候为满,什么时候为空

  • Task任务类

    #pragma once
#include <iostream>
#include <string>
#include <unistd.h>class Task
{
public:Task(){}Task(int x, int y, char op) : _x(x), _y(y), _op(op), _result(0), _exitCode(0){}void operator()(){switch (_op){case '+':_result = _x + _y;break;case '-':_result = _x - _y;break;case '*':_result = _x * _y;break;case '/':{if (_y == 0)_exitCode = -1;else_result = _x / _y;}break;case '%':{if (_y == 0)_exitCode = -2;else_result = _x % _y;}break;default:break;}usleep(100000);}std::string formatArg(){return std::to_string(_x) + _op + std::to_string(_y) + "= ?";}std::string formatRes(){return std::to_string(_result) + "(" + std::to_string(_exitCode) + ")";}~Task(){}private:int _x;int _y;char _op;int _result;int _exitCode;
};
  • 生产和消费处理函数
void *consumerRoutine(void *args)
{RingQueue<Task> *rq = static_cast<RingQueue<Task> *>(args);while (true){Task t;rq->pop(&t);t();cout << "consumer done, 处理完成的任务是: " << t.formatRes() << endl;}
}void *productorRoutine(void *args)
{RingQueue<Task> *rq = static_cast<RingQueue<Task> *>(args);while (true){// sleep(1);int x = rand() % 100;int y = rand() % 100;char op = ops[(x + y) % strlen(ops)];Task t(x, y, op);rq->push(t);cout << "productor done, 生产的任务是: " << t.formatArg() << endl;}
}
  • Main主函数
int main()
{
RingQueue rq;
pthread_t c,p;
pthread_create(&c, NULL, consumer, (void*)&rq);
pthread_create(&p, NULL, producter, (void*)&rq);
pthread_join(c, NULL);
pthread_join(p, NULL);
}

1.3 两种实现的区别

  • 基于阻塞队列实现的生产消费模型和环形队列的实现,最大的区别是否让生产和消费的过程并发起来。

2 .线程池的实现(懒汉模式)

线程池是基于阻塞队列实现的。

  • 我们只需要把任务交到这个线程的池子里面,其就能帮我们多线程执行任务,计算出结果。
  • 当任务来时才创建线程,这个成本有点高,如果提前先把各种池化的东西准备好,等任务来的时候,直接把任务指派给某个线程。
  • 无论是进程池还是线程池,本质上都是一种对于执行流的预先分配,当有任务时,直接指定,而不需要创建进程/线程来处理任务

线程池

一种线程使用模式。线程过多会带来调度开销,进而影响缓存局部性和整体性能。而线程池维护着多个线程,等待着
监督管理者分配可并发执行的任务。这避免了在处理短时间任务时创建与销毁线程的代价。线程池不仅能够保证内核的充分利
用,还能防止过分调度。可用线程数量应该取决于可用的并发处理器、处理器内核、内存、网络sockets等的数量

线程池的应用场景

  1. 需要大量的线程来完成任务,且完成任务的时间比较短。 WEB服务器完成网页请求这样的任务,使用线程池技 术是非常合适的。因为单个任务小,而任务数量巨大,你可以想象一个热门网站的点击次数。 但对于长时间的任务,比如一个
    Telnet连接请求,线程池的优点就不明显了。因为Telnet会话时间比线程的创建时间大多了。
    1. 对性能要求苛刻的应用,比如要求服务器迅速响应客户请求。
    1. 接受突发性的大量请求,但不至于使服务器因此产生大量线程的应用。突发性大量客户请求,在没有线程池情 况下,将产生大量线程,虽然理论上大部分操作系统线程数目最大值不是问题,短时间内产生大量线程可能使内存到达极限, 出现错误.

2.1 单例模式复习:

在我们之前学过的单例模式分为两种,一种是懒汉模式,一种是饿汉模式 [传送门] 。

  • 懒汉:刚开始先不创建对象,等第一次使用的时候再去创建。
    缺点:是第一次创建对象需要等待。
    优点:是程序启动快。
  • 饿汉:在main函数之前就将对象创建出来。
    缺点:是程序启动会比较慢。
    优点:是启动之后获取对象会比较快。

2.2 线程池成员变量:

#pragma once#include <iostream>
#include <cassert>
#include <queue>
#include <memory>
#include <cstdlib>
#include <pthread.h>
#include <unistd.h>
#include <sys/prctl.h>
#include "Log.hpp"
#include "Lock.hpp"
using namespace std;
const static int N = 5;template <class T>
class ThreadPool
{
private:int _num; //线程池中线程的个数bool isStart_; // 表示是否已经启动std::queue<T> _tasks; // 使用stl的自动扩容的特性 基于阻塞队列进行实现的(里面是放置Task的)pthread_mutex_t _lock;pthread_cond_t _cond;static ThreadPool<T> *instance; //懒汉模式的实例static pthread_mutex_t instance_lock;//懒汉模式的锁
};

static变量我们需要在类外初始化,模板类型还需要带上template关键字:

template <class T>
ThreadPool<T> *ThreadPool<T>::instance = nullptr;template <class T>
pthread_mutex_t ThreadPool<T>::instance_lock = PTHREAD_MUTEX_INITIALIZER;

2.3 构造和析构函数

private:ThreadPool(int num = N) : _num(num), isStart_(false){assert(threadNum_ > 0);pthread_mutex_init(&_lock, nullptr);pthread_cond_init(&_cond, nullptr);}ThreadPool(const ThreadPool<T> &tp) = delete;void operator=(const ThreadPool<T> &tp) = delete;public:~ThreadPool(){pthread_mutex_destroy(&_lock);pthread_cond_destroy(&_cond);}

封装加锁/解锁/通知线程等操作:

private:
void LockQueue() {pthread_mutex_lock(&_lock);
} 
void UnLockQueue() {pthread_mutex_unlock(&_lock);
} 
void WakeUpOne() {pthread_cond_signal(&_cond);
} 
void WakeUpAll() {pthread_cond_broadcast(&_cond);
} 
void ThreadQuit() {_thread_cur--;UnLockQueue();pthread_exit(NULL);
} 
void ThreadWait(){pthread_cond_wait(&_cond, &_lock);
} 
bool IsEmpty() {return _task_queue.empty();}

因为是懒汉模式的单例,提供一个指针作为单例,不对外开放构造函数。
同时,用delete关键字,禁止拷贝构造和赋值重载

public:static ThreadPool<T> *getinstance(){if(nullptr == instance) // 为什么要这样?提高效率,减少加锁的次数!{LockGuard lockguard(&instance_lock);if (nullptr == instance){instance = new ThreadPool<T>();instance->start();}}return instance;}

2.4 线程池启动

 static void threadRoutine(void *args){// pthread_detach(pthread_self());ThreadPool<T> *tp = static_cast<ThreadPool<T> *>(args);while (true){// 1. 检测有没有任务// 2. 有:处理// 3. 无:等待// 细节:必定加锁T t;//定义的地方{tp->LockQueue();while (tp->isEmpty()){tp->threadWait();}t = tp->popTask(); // 从公共区域拿到私有区域}tp->UnLockQueue();// for test// for debugint one, two;char oper;t.get(&one, &two, &oper);std::cout << "thread handler done, result: " << t.formatRes() << std::endl;// t.run(); // 处理任务,应不应该在临界区中处理?1,0}}
void start()
{// 作为一个线程池,不能被重复启动assert(!isStart_);for (int i = 0; i < _num; i++){pthread_t temp;pthread_create(&temp, nullptr, threadRoutine, this);}isStart_ = true;
}

2.5 Pop和Push Task任务

void PushTask(const T &t) {LockQueue();_tasks.push(t);WakeUpOne();UnLockQueue();
}
T  PopTask() 
{T t = _tasks.front();_tasks.pop();return t;
}

2.6 Task类

  #pragma once
#include <iostream>
#include <string>
#include <unistd.h>class Task
{
public:Task(){}Task(int x, int y, char op) : _x(x), _y(y), _op(op), _result(0), _exitCode(0){}void operator()(){switch (_op){case '+':_result = _x + _y;break;case '-':_result = _x - _y;break;case '*':_result = _x * _y;break;case '/':{if (_y == 0)_exitCode = -1;else_result = _x / _y;}break;case '%':{if (_y == 0)_exitCode = -2;else_result = _x % _y;}break;default:break;}usleep(100000);}std::string formatArg(){return std::to_string(_x) + _op + std::to_string(_y) + "= ?";}std::string formatRes(){return std::to_string(_result) + "(" + std::to_string(_exitCode) + ")";}~Task(){}private:int _x;int _y;char _op;int _result;int _exitCode;
};

2.7 Main.cc的实现:

int main()
{const string operatorsZZ = "+/*/%";// unique_ptr<ThreadPool<Task> > tp(new ThreadPool<Task>());// 懒汉模式之后这个就不能用了unique_ptr<ThreadPool<Task>> tp(ThreadPool<Task>::getInstance());srand((unsigned long)time(nullptr));// 派发任务的线程while (true){int one = rand() % 50;int two = rand() % 10;char oper = operatorsZZ [rand() % operatorsZZ .size()];Log() << "主线程派发计算任务: " << one << oper << two << "=?"<< "\n";Task t(one, two, oper);tp->push(t);sleep(1);}return 0;
}

3. STL,智能指针和线程安全

  • STL中的容器是否是线程安全的?

不是. 原因是, STL 的设计初衷是将性能挖掘到极致, 而一旦涉及到加锁保证线程安全, 会对性能造成巨大的影响.
而且对于不同的容器,加锁方式的不同, 性能可能也不同(例如hash表的锁表和锁桶).
因此 STL 默认不是线程安全. 如果需要在多线程环境下使用,往往需要调用者自行保证线程安全.

  • 智能指针是否是线程安全的?

对于 unique_ptr, 由于只是在当前代码块范围内生效, 因此不涉及线程安全问题.(因为不能拷贝和赋值,只能自己用)
对于 shared_ptr,多个对象需要共用一个引用计数变量, 所以会存在线程安全问题. 但是标准库实现的时候考虑到了这 个问题, 基于原子操作(CAS)的方式保证 shared_ptr 能够高效, 原子的操作引用计数

尾声
看到这里,相信大家对这个Linux有了解了。
如果你感觉这篇博客对你有帮助,不要忘了一键三连哦

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

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

相关文章

国科大作业考试资料《人工智能原理与算法》2024新编-第十三次作业整理

1、假设我们从决策树生成了一个训练集&#xff0c;然后将决策树学习应用于该训练集。当训练集的大小趋于无穷时&#xff0c;学习算法将最终返回正确的决策树吗&#xff1f;为什么是或不是&#xff1f; 本次有两个参考&#xff1a; 参考一&#xff1a; 当训练集的大小趋于无穷…

普中51单片机:蜂鸣器的简单使用(十一)

文章目录 引言蜂鸣器的分类工作原理无源蜂鸣器压电式蜂鸣器&#xff1a;电磁式蜂鸣器&#xff1a; 电路符号及应用代码演示——无源蜂鸣器 引言 蜂鸣器是一种常见的电子音响器件&#xff0c;广泛应用于各种电子产品中。它们能够发出不同频率的声音&#xff0c;用于警报、提醒、…

整数二分详解【附带PPT】

#include<bits/stdc.h> using namespace std; int n,a[1001],k;int b_search1(int l,int r,int k){while(l<r){int mlr1>>1;//检查是否满足橙色性质 if(a[m]<k) lm;else rm-1;}//循环结束l和r同时指向边界 return l; }int b_search2(int l,int r,int k){whil…

【Linux】进程间通信(1):进程通信概念与匿名管道

人与人之间是如何通信的&#xff1f;举个简单的例子&#xff0c;假如我是月老&#xff0c;我要为素不相识的但又渴望爱情的男女两方牵红线。我需要收集男方的信息告诉女方&#xff0c;收集女方的信息告诉男方&#xff0c;然后由男女双方来决定是否继续。对于他们而言&#xff0…

Python | Leetcode Python题解之第275题H指数II

题目&#xff1a; 题解&#xff1a; class Solution:def hIndex(self, citations: List[int]) -> int:n len(citations)left 0; right n - 1while left < right:mid left (right - left) // 2if citations[mid] > n - mid:right mid - 1else:left mid 1retur…

【Linux C | 网络编程】进程池大文件传输的实现详解(三)

上一篇实现了进程池的小文件传输&#xff0c;使用自定义的协议&#xff0c;数据长度数据本身&#xff0c;类似小火车的形式&#xff0c;可以很好的解决TCP“粘包”的问题。 【Linux C | 网络编程】进程池小文件传输的实现详解&#xff08;二&#xff09; 当文件的内容大小少于…

html+css 动态效果

1.波浪效果 <div class"sitesingle"></div> <style>.sitesingle{display:flex;justify-content:space-between;align-items:stretch;overflow:hidden;position:relative;height: 400px;}keyframes bgRotate{0%{transform:rotate(0deg)}to{transfor…

基于关联规则的分类算法(CBA) | 项集、频繁项集、关联规则 | arulesCBA库

基于关联规则的分类算法 目前使用较多且较为简洁的关联规则分类算法是基于关联规则的分类算法&#xff08;Classification Based on Association, CBA&#xff09;&#xff0c;下面将从该算法的相关概念开始介绍。 这部分笔记参考论文&#xff1a;孙菡悦.基于多因素交互效应的…

C++的STL简介(一)

目录 1.什么是STL 2.STL的版本 3.STL的六大组件 4.string类 4.1为什么学习string类&#xff1f; 4.2string常见接口 4.2.1默认构造 ​编辑 4.2.2析构函数 Element access: 4.2.3 [] 4.2.4迭代器 ​编辑 auto 4.2.4.1 begin和end 4.2.4.2.regin和rend Capacity: 4.2.5…

repo中的default.xml文件project name为什么一样?

文章目录 default.xml文件介绍为什么 name 是一样的&#xff0c;path 不一样&#xff1f;总结 default.xml文件介绍 在 repo 工具的 default.xml 文件中&#xff0c;定义了多个 project 元素&#xff0c;每个元素都代表一个 Git 仓库。 XML 定义了多个不同的 project 元素&…

树和二叉树(不用看课程)

1. 树 1.1 树的概念与结构 树是⼀种非线性的数据结构&#xff0c;它是由 n&#xff08;n>0&#xff09; 个有限结点组成⼀个具有层次关系的集合。把它叫做树是因为它看起来像⼀棵倒挂的树&#xff0c;也就是说它是根朝上&#xff0c;而叶朝下的。 • 有⼀个特殊的结点&am…

GD32相较于STM32的优劣势-完全总结

优势 1.更高的主频 GD32单片机的主频可以达到108MHz&#xff0c;‌而STM32的最大主频为72MHz&#xff0c;‌这意味着GD32在代码执行速度上具有优势&#xff0c;‌适合需要快速处理数据的场景 2.更低的内核电压 GD32的内核电压为1.2V&#xff0c;‌而STM32的内核电压为1.8V。…

《系统架构设计师教程(第2版)》第12章-信息系统架构设计理论与实践-05-信息系统架构案例分析

文章目录 1. 价值驱动的体系结构——连接产品策略与体系结构1.1 价值模型1&#xff09;概述2&#xff09;价值驱动因素3&#xff09;传统方法识别价值模型的缺陷&#xff08;了解即可&#xff09; 1.2 体系结构策略&#xff08;挑战&#xff09;1&#xff09; 优先级的确定2&am…

【C++】动态内存管理与模版

目录 1、关键字new&#xff1a; 1、用法&#xff1a; 2、理解&#xff1a; 3、与malloc的相同与不同&#xff1a; 1、相同&#xff1a; 2、不同&#xff1a; 2、模版初阶&#xff1a; 1、函数模版&#xff1a; 1、概念&#xff1a; 2、关键字&#xff1a;template&…

微信公众号获取用户openid(PHP版,snsapi_base模式)

微信公众号获取用户openid的接口有2个&#xff1a;snsapi_base、snsapi_userinfo 详情见微信公众号开发文档&#xff1a;https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/Wechat_webpage_authorization.html 本文介绍用PHP方式调用snsapi_base接口获取微信用户…

苦学Opencv的第十一天:图像的形态学操作

Python OpenCV从入门到精通学习日记&#xff1a;图像的形态学操作 前言 图像形态学是图像处理中的一个重要分支&#xff0c;主要关注图像中物体的形状和结构。通过形态学操作&#xff0c;我们可以对图像进行有效的分析和处理&#xff0c;例如图像的腐蚀与膨胀、开运算与闭运算…

ansible基础讲解和加密文件讲解

ansible最重要的三个文件 /etc/ansible/ansible.cfg #####ansible的配置文件 /etc/ansible/host ##清单文件inventory ansible-navigator.yml ####以yml结尾的文件可以理解为conf结尾的文件&#xff0c;是配置文件&#xff0c;用于设置剧本playbook playbook讲解 以.yml结…

vue3中计算属性

假如需要修改,需要使用get,set let a ref(111) import {computed} from vue let changeimg computed({get(){return a},set(val){a.value val}}) 如果不需要修改 let a ref(111) import {computed} from vue let changeimg computed(() >{return a })

135.分发糖果,遍历方向+candy选取的详解

力扣135分发糖果 题目思路代码 题目 https://leetcode.cn/problems/candy/description/ 老师想给孩子们分发糖果&#xff0c;有 N 个孩子站成了一条直线&#xff0c;老师会根据每个孩子的表现&#xff0c;预先给他们评分。 你需要按照以下要求&#xff0c;帮助老师给这些孩子…

WordPress原创插件:自定义文章标题颜色

插件设置截图 文章编辑时&#xff0c;右边会出现一个标题颜色设置&#xff0c;可以设置为任何颜色 更新记录&#xff1a;从输入颜色css代码&#xff0c;改为颜色选择器&#xff0c;更方便&#xff01; 插件免费下载 https://download.csdn.net/download/huayula/89585192…