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

news2024/11/24 4:36:34

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

目录

    • 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_;            // blockqueue
    pthread_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;  // 衡量空间计数器,productor
       sem_t space_sem; // 衡量数据计数器,consumer
       int 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 debug
           int 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.coloradmin.cn/o/1953793.html

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

相关文章

springboot电子产品销售系统-计算机毕业设计源码80294

摘 要 电子商务行业在全球范围内迅速发展&#xff0c;随之而来的是电子产品销售市场的快速增长和消费者对在线购物体验的需求提升&#xff0c;因此&#xff0c;电子产品销售系统应运而生。该系统旨在满足电子产品市场的需求&#xff0c;提供全面的购物功能和高效的管理操作。 …

严格模式 模块化开发

严格模式 当你在脚本或函数的顶部添加"use strict"语句时&#xff0c;你的代码将在严格模式下执行。这可以帮助你避免某些常见的编程陷阱&#xff0c;例如在不声明变量的情况下就使用它们&#xff0c;或者删除变量、函数或函数参数。在严格模式下&#xff0c;这样的…

进阶篇,内附代码:锂电池二阶模型-离线与在线参数辨识

锂电池二阶模型-在线参数辨识 背景二阶等效电路模型介绍二阶模型的离线参数辨识二阶模型的RLS表达式递推代码已知问题背景 锂电池一阶戴维南等效模型的参数辨识方法,已经在前面两期详细地讲解了一轮。 一阶模型-离线参数辨识一阶模型-在线参数辨识本期继续讲解一下如何进行二…

stm32——lcd液晶显示

一.液晶屏介绍 液晶显示屏是由液晶显示面板&#xff0c;电容触摸屏&#xff0c;pcb底板构成。在液晶显示屏里我们有带控制芯片的还有不带控制芯片的。对于低端的微控制器它不能直接控制液晶面板&#xff0c;所以需要给液晶控制面板而外增加一个液晶控制芯片。对于单片机stm32f4…

Photos框架 - 自定义媒体选择器(UI预览)

引言 在前面的博客中我们已经介绍了使用媒体资源数据的获取&#xff0c;以及自定义的媒体资源选择列表页。在一个功能完整的媒体选择器中&#xff0c;预览自然是必不可少的&#xff0c;本篇博客我们就来实现一个资源的预览功能&#xff0c;并且实现列表和预览的数据联动效果。…

内网横向——远程桌面利用

文章目录 一、远程桌面的确定和开启二、RDP Hijacking 网络拓扑&#xff1a; 攻击机kali IP&#xff1a;192.168.111.0 跳板机win7 IP&#xff1a;192.168.111.128&#xff0c;192.168.52.143 靶机win server 2008 IP&#xff1a;192.168.52.138 一、远程桌面的确定和开启 下面…

VMware、Docker - 让虚拟机走主机代理,解决镜像封禁问题

文章目录 虚拟机全局代理配置找到 VMnet8 的 IPv4 地址代理相关配置虚拟机代理配置 Docker 代理配置修改镜像修改 Docker 代理配置 虚拟机全局代理配置 找到 VMnet8 的 IPv4 地址 a&#xff09;打开此电脑&#xff0c;输入 “控制面板”&#xff0c;然后回车. b&#xff09;之…

元注解相关知识总结

Target 这个注解适用于决定注解的适用范围&#xff0c;例如适用于构造器&#xff0c;方法&#xff0c;字段等 Retention 这个注解的作用是确定注解的生命周期一般用得比较多的是RunTime这样就可以在运行环境中使用它&#xff0c;赋值的方式一般是使用value进行赋值 SOURCE 代…

【初阶数据结构篇】顺序表的实现(赋源码)

文章目录 本篇代码位置顺序表和链表1.线性表2.顺序表2.1 概念与结构2.2分类2.2.1 静态顺序表2.2.2 动态顺序表 2.3 动态顺序表的实现2.3.1动态顺序表的初始化和销毁及打印2.3.2动态顺序表的插入动态顺序表的尾插动态顺序表的头插动态顺序表的在指定位置插入数据 2.3.3动态顺序表…

spring常用注解有哪些

Spring框架使用了大量的注解来简化配置和开发&#xff0c;以下是一些常用的Spring注解&#xff1a; 1.Component&#xff1a;通用的构造型注解&#xff0c;用于标记一个类作为Spring管理的组件&#xff0c;通常用于自定义组件。 2.Autowired&#xff1a;用于自动装配Bean&#…

Android Add a Header to the GridLayout

RecyclerView 添加 Header, RecycleView 设置 LayoutManager 为 GridLayoutManger的 时候&#xff1b; Header 要占用3行 val manager GridLayoutManager(activity, 3)binding.sleepList.layoutManager manager// span size for each position, and assign it to manager.spa…

Golang零基础入门课_20240726 课程笔记

视频课程 最近发现越来越多的公司在用Golang了&#xff0c;所以精心整理了一套视频教程给大家&#xff0c;这个只是其中的第一部&#xff0c;后续还会有很多。 视频已经录制完成&#xff0c;完整目录截图如下&#xff1a; 课程目录 01 第一个Go程序.mp402 定义变量.mp403 …

github简单地操作

1.调节字体大小 选择options 选择text 选择select 选择你需要的参数就可以了。 2.配置用户名和邮箱 桌面右键&#xff0c;选择git Bash Here git config --global user.name 用户名 git config --global user.email 邮箱名 3.用git实现代码管理的过程 下载别人的项目 git …

PlatformIO+ESP32S3学习:通过WIFI与和风天气API获取指定地点的天气情况并显示

1. 硬件准备 你只需要有一个ESP32S3开发板。我目前使用的是&#xff1a; 购买地址&#xff1a;立创ESP32S3R8N8 开发板 2. 和风天气API 2.1. 和风天气介绍 和⻛天气是中国领先的气象科技服务商、国家高新技术 企业&#xff0c;致力于运用先进气象模型结合大数据、人工智能 技术…

Halcon 引擎方式调试

1.C# 端添加代码 启动调试模式 public HDevEngine MyEngine new HDevEngine(); // halcon引擎;// 启动调试服务 MyEngine.StartDebugServer();2.Halcon程序添加到进程 打开Halcon程序 【执行】>【附加到进程】 点击【确定】 3.C# 程序执行到相关位置 C# 程序执行调用…

大模型学习笔记十四:Agent模型微调

文章目录 一、大模型需要Agent技术的原因二、Prompt Engineering可以实现Agent吗&#xff1f;&#xff08;1&#xff09;ReAct原理展示和代码&#xff08;2&#xff09;ModelScope&#xff08;3&#xff09;AutoGPT&#xff08;4&#xff09;ToolLLaMA 三、既然AutoGPT可以满足…

Java SE 文件上传和文件下载的底层原理

1. Java SE 文件上传和文件下载的底层原理 文章目录 1. Java SE 文件上传和文件下载的底层原理2. 文件上传2.1 文件上传应用实例2.2 文件上传注意事项和细节 3. 文件下载3.1 文件下载应用实例3.2 文件下载注意事项和细节 4. 总结&#xff1a;5. 最后: 2. 文件上传 文件的上传和…

【C语言】指针基础知识理解

1. 内存和地址 1.1 内存 在讲内存和地址之前&#xff0c;我们想有个⽣活中的案例&#xff1a; 假设你在学校宿舍楼&#xff0c;但是宿舍的房间没有编号&#xff0c;你的⼀个朋友发微信来找你玩&#xff0c;如果想找到你&#xff0c;就得挨个房⼦去找&#xff0c;这样效率会很…

《算法笔记》总结No.11——数字处理(上)欧拉筛选

机试中存在部分涉及到较复杂数字的问题&#xff0c;这是编码的基本功&#xff0c;各位一定要得心应手。 目录 一.最大公约数和最小公倍数 1.最大公约数 2.最小公倍数 二.素数 1.判断指定数 2.输出所有素数 3.精进不休——埃拉托斯特尼筛法 4.达到更优&#xff01;——…

STL-string(使用和部分模拟实现)

1.string basic_string<char> 是 C 标准库中定义的一个模板类型,用于表示一个字符串。这个模板类接收一个字符类型作为模板参数。typedef basic_string<char> string&#xff1a;string类是basic_string类模板的实例化&#xff0c;它使用 char作为其字符类型。 2.…