Linux多线程---生产者消费者模型和线程池

news2025/5/26 10:55:56

目录

生产者消费者模型

条件变量

接口

实现基于阻塞队列的cp模型

POSIX信号量

信号量的PV操作

接口:

初始化信号量:

销毁信号量

 等待信号量

发布信号量

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

线程池

应用场景:

如何实现?


生产者消费者模型

生产者消费者模型的作用:生产者消费者模式本质就是通过一个容器来解决生产者消费者强耦合的问题。

这个容器可以是队列或链表,充当着缓冲区的作用 ,生产者和消费者彼此不直接通讯,而通过阻塞队列来通讯,生产者生产完数据不用等待消费者获取,而是直接放入阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列中获取数据,阻塞队列平衡了生产者消费者的处理能力,将生产者和消费者解耦。

生产者消费者模型是同步与互斥的典型应用场景。

生产者消费者模型优点
解耦
支持并发
支持忙闲不均

队列或链表在内存中是消费者和生产者之间的缓冲区。

缓冲区作用是提高生产消费效率和将生产者消费者解耦,同时缓冲区是临界资源,操作时需要加锁保护。

这个模型下有一个交易场所(缓冲区),两种角色(生产者和消费者),三种关系。(简记321原则)

三种关系

生产者之间:互斥

消费者之间:互斥

消费者与生产者之间:互斥,同步 

为什么存在互斥关系?

因为生产线程和消费线程都会访问缓冲区,缓冲区是临界资源,生产者消费者都会对临界资源做操作,所以访问临界资源需要加锁保护,所以无论是生产线程还是消费线程访问之前都需要先申请锁,所以生产者之间,消费者之间,消费者与生产者之间都需要竞争锁,所以它们都具有互斥关系。

为什么消费者与生产者之间存在同步关系?

当生产者生产满了数据在队列中,就不能继续生产,否则生产者一直占有锁,就会造成消费者饥饿问题,当消费者消费完了队列中的数据,队列为空就不能继续消费,消费者一直占有锁会造成生产者饥饿问题,所以应该让消费者和生产者访问队列具有一定的顺序性,比如队列空了就先生产,队列满了就先消费,这个就是线程之间的同步关系。

那么如何让多个生产者,消费者在我们指定的条件下等待或唤醒?

使用条件变量。 

条件变量

接口

条件变量特点:唤醒线程由系统唤醒变成让程序员唤醒

初始化条件变量:pthread_cond_init

销毁条件变量:pthread_cond_destroy

 初始化方法与pthread_mutex_t相似,可以使用宏初始化全局变量,也可以使用函数初始化。

让线程阻塞等待条件就绪:

第一参数是条件变量,表示指定条件。

第二个参数是锁,是线程阻塞等待时会释放的锁,线程被唤醒时被申请的锁。 

pthread_cond_wait在特定条件下阻塞等待

pthread_cond_timedwait等待指定时间函数返回

 pthread_cond_timedwait和pthread_cond_wait的区别是 pthread_cond_timedwait会阻塞等待abstime长的时间,到时后线程如果没被唤醒函数就会返回错误码。

注意:

处于等待条件下不能被pthread_cancel。

当线程在条件未就绪等待时,线程阻塞等待时线程会自动释放锁,当线程被唤醒时会自动申请锁。

唤醒指定条件下等待的线程:

pthread_cond_signal:唤醒一个在指定条件等待的线程

pthread_cond _broadcast:唤醒所有在指定条件下等待的线程

唤醒线程函数在唤醒多个线程类似于排队,多个线程会被按一定顺序依次唤醒。 

实现基于阻塞队列的cp模型

阻塞队列与普通队列区别:当队列为空的时候,从队列获取元素的操作将会被阻塞,当队列为满的时候,往队列存放元素的操作也会被阻塞,类似管道。

实现单生产者,单消费者版本:

封装blockQueue类

队列

容量:队列最大数据个数

互斥锁

条件变量:保持生产者消费者同步,避免生产饥饿或消费饥饿问题

生产接口

加锁

判断队列是否满了

满了,不生产,在条件下等待(休眠)

不满,生产,唤醒消费者

生产

解锁 

消费接口

加锁

判断

空,不消费,在条件下等待(休眠)

不空,消费,唤醒生产者

消费

解锁

注意:

线程被唤醒了并不代表条件一定满足(伪唤醒或阻塞等待调用失败等),需要再检测条件是否满足

在临界区唤醒线程或临界区外唤醒线程都行

代码实现:

BlockQueue.h:

#include <iostream>
#include <queue>
#include <pthread.h>
#include <unistd.h>
using namespace std;

template <class T>
class BlockQueue
{
private:
    queue<T> q_;
    int capacity_;
    pthread_mutex_t mutex_;
    pthread_cond_t full_;
    pthread_cond_t empty_;
    bool isFull() { return q_.size() == capacity_; }
    bool isEmpty() { return q_.empty(); }

public:
    BlockQueue(int capacity)
        : capacity_(capacity)
    {
        pthread_mutex_init(&mutex_, nullptr);
        pthread_cond_init(&full_, nullptr);
        pthread_cond_init(&empty_, nullptr);
    }
    void push(T &t)
    {
        pthread_mutex_lock(&mutex_);
        while (isFull()) // 当队列满的时候,生产线程在full_条件下等待。
        {
            pthread_cond_wait(&full_, &mutex_); // 函数在线程阻塞的时候会自动释放锁,在线程被唤醒的时候会自动申请锁
        }
        q_.push(t);
        pthread_mutex_unlock(&mutex_);
        pthread_cond_signal(&empty_); // 生产者生产完数据,唤醒在empty_条件下等待的消费者
    }
    void pop(T &data) // 输出型参数
    {
        pthread_mutex_lock(&mutex_);
        while (isEmpty()) // 当队列空的时候,消费线程在empty_条件下等待。
        {
            pthread_cond_wait(&empty_, &mutex_); // 函数在线程阻塞的时候会自动释放锁,在线程被唤醒的时候会自动申请锁
        }
        data = q_.front();
        q_.pop();
        pthread_mutex_unlock(&mutex_);
        pthread_cond_signal(&full_); // 消费者消费完数据,唤醒在full_条件下等待的生产者
    }
};

 测试代码:

#include "BlockQueue.h"

void *pRoutine(void *args)
{
    BlockQueue<int> *bq = static_cast<BlockQueue<int> *>(args);
    while (1)
    {
        int data = rand() % 1000;
        bq->push(data);
        cout << "生产者生产了数据:" << data << endl;
    }
    return nullptr;
}

void *cRoutine(void *args)
{
    BlockQueue<int> *bq = static_cast<BlockQueue<int> *>(args);
    while (1)
    {
        int data = -1;
        bq->pop(data);
        cout << "消费者消费了数据:" << data << endl;
        sleep(1);
    }
    return nullptr;
}

int main()
{
    BlockQueue<int> bq(5);
    pthread_t p, c;
    pthread_create(&p, nullptr, pRoutine, &bq);
    pthread_create(&c, nullptr, cRoutine, &bq);
    pthread_join(p, nullptr);
    pthread_join(c, nullptr);
    return 0;
}

输出:

 

可以看出一开始队列为空,生产者一次性生产满了队列,队列满了就不生产了, 而后消费者消费一个数据生产者生产一个数据,也就是生产者消费者保持同步了。

支持并发 在队列中的访问线程们一定是串行的,支持并发主要是体现在生产者们同时制作任务,消费者们同时消费任务。   
支持忙闲不均 指的是任务放入队列前和从队列获取任务后,生产线程和消费线程各自做着自己的工作。

POSIX信号量

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

信号量本质:信号量是一个计数器——描述临界资源数量的计数器

申请信号量的预定机制:只要信号量申请成功,就一定会获得指定的资源。

当临界资源数量就一个,信号量要么为0要么为1,就两种状态称为二元信号量,即互斥锁。

信号量的PV操作

P操作:申请信号量,即申请临界资源,将信号量--。

V操作:释放信号量,即释放临界资源,将信号量++。

多线程在申请信号量,同时访问信号量,所以信号量是临界资源,所以操作临界资源必须是原子操作才不出问题,即PV操作需要是原子操作,这点已经由内核实现保证了。

接口:

头文件:semaphore.h

初始化信号量:

参数:
pshared:0 表示线程间共享,非零表示进程间共享
value :信号量初始值

销毁信号量

int sem_destroy(sem_t *sem);

 等待信号量

int sem_wait(sem_t *sem);

即信号量的P操作,申请信号量

发布信号量

int sem_post(sem_t *sem);

即信号量的V操作,释放信号量

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

我们知道用数组实现环形队列会用两个头尾指针,当队列为满或空,指针指向同一个位置 ,那么如何分辨这两种状态呢?

答案是不需要分辨,使用信号量帮我们维护就行了。

 除了上述两种情况,其他时候指针指向不同的位置,这时生产线程消费线程是并发的,因为可以让生产者和消费者同时访问队列的不同区域,生产的同时也在消费。

实现时还需要两个原则:

原则一:生产者和消费者不能对同一个位置进行访问,因为生产者消费者是互斥的。

这个原则信号量可以保证。

对于生产者而言:关心空间(room),空间一开始为N(环形队列大小)

对于消费者而言:关心数据(data),数据一开始为0,起初没有数据

原则二:

队列为空:消费者不能超过生产者,没有数据时不能消费,要让生产者先运行

队列为满:生产者不能把消费者套圈,即生产者不能继续写入 ,要让消费者先运行

程序员编码实现:不同线程访问临界资源的不同区域

代码实现:

RingQueue.h:

#pragma once
#include <semaphore.h>
#include <vector>
#include <iostream>
#include <unistd.h>
#define CAPACITY 8
using namespace std;
template <class T>
class RingQueue
{
public:
    RingQueue(uint32_t capacity = CAPACITY)
        : _ringQueue(capacity), _capacity(capacity), _pIndex(0), _cIndex(0)
    {
        sem_init(&_roomSem, 0, _capacity);
        sem_init(&_dataSem, 0, 0);
    }
    ~RingQueue()
    {
        sem_destroy(&_roomSem);
        sem_destroy(&_dataSem);
    }
    void push(T &data) // 生产者生产数据
    {
        sem_wait(&_roomSem); // 申请空间资源,--roomSem
        _ringQueue[_pIndex] = data;
        sem_post(&_dataSem); // 释放数据资源,++dataSem
        //++_pIndex放后面做,因为上面是临界区,尽量不在临界区做多余的事情
        _pIndex++;
        _pIndex %= _capacity;
    }
    void pop(T &data) // 消费者消费数据,输出型参数
    {
        sem_wait(&_dataSem); // 申请数据资源,--dataSem
        data = _ringQueue[_cIndex];
        sem_post(&_roomSem); // 释放空间资源,++roomSem
        //++_cIndex放后面做,因为上面是临界区,尽量不在临界区做多余的事情
        ++_cIndex;
        _cIndex %= _capacity;
    }

private:
    vector<T> _ringQueue; // 环形队列
    uint32_t _capacity;   // 环形队列大小
    sem_t _roomSem;       // 空间资源个数
    sem_t _dataSem;       // 数据资源个数
    uint32_t _pIndex;     // 表示生产者正在生产的位置
    uint32_t _cIndex;     // 表示消费者正在消费的位置
};

RingQueue.cc:

#include "RingQueue.h"

void *pRoutine(void *args)
{
    RingQueue<int> *rq = static_cast<RingQueue<int> *>(args);
    while (1)
    {
        int data = rand() % 1000;
        rq->push(data);
        cout << "生产者生产了一个数据:" << data << endl;
    }
    return nullptr;
}

void *cRoutine(void *args)
{
    RingQueue<int> *rq = static_cast<RingQueue<int> *>(args);
    while (1)
    {
        int data = -1;
        rq->pop(data);
        cout << "消费者消费了一个数据:" << data << endl;
        sleep(1);
    }
    return nullptr;
}
int main()
{
    RingQueue<int> rq(6);
    pthread_t p, c;
    pthread_create(&p, nullptr, pRoutine, &rq);
    pthread_create(&c, nullptr, cRoutine, &rq);
    pthread_join(p, nullptr);
    pthread_join(c, nullptr);
    return 0;
}

 运行结果:

如果放慢生产节奏,消费者也会跟着变慢,这是同步的体现。

倘若要实现多生产多消费者,关键是要让生产者之间互斥,消费者之间互斥,只需要生产者消费者分别加锁即可。

注意:

在申请信号量之后加锁

在释放信号量之前解锁

为什么如此加锁?因为处理数据或任务要花时间,任何时候访问队列只有一个生产线程和消费线程,我们可以利用信号量的预订机制让线程先申请信号量,即先预订资源效率更高。

代码实现:

 RingQueue.h:

#pragma once
#include <semaphore.h>
#include <vector>
#include <iostream>
#include <unistd.h>
#define CAPACITY 8
using namespace std;
template <class T>
class RingQueue
{
public:
    RingQueue(uint32_t capacity = CAPACITY)
        : _ringQueue(capacity), _capacity(capacity), _pIndex(0), _cIndex(0)
    {
        sem_init(&_roomSem, 0, _capacity);
        sem_init(&_dataSem, 0, 0);
        pthread_mutex_init(&_pmutex,nullptr);
        pthread_mutex_init(&_cmutex,nullptr);
    }
    ~RingQueue()
    {
        sem_destroy(&_roomSem);
        sem_destroy(&_dataSem);
        pthread_mutex_destroy(&_pmutex);
        pthread_mutex_destroy(&_cmutex);
    }
    void push(T &data) // 生产者生产数据
    {
        sem_wait(&_roomSem); // 申请空间资源,--roomSem

        pthread_mutex_lock(&_pmutex);
        _ringQueue[_pIndex] = data;
        _pIndex++;
        _pIndex %= _capacity;
        cout << "生产者[" << pthread_self() << "]:生产了一个数据:" << data << endl;//便于观察
        pthread_mutex_unlock(&_pmutex);

        sem_post(&_dataSem); // 释放数据资源,++dataSem
    }
    void pop(T &data) // 消费者消费数据,输出型参数
    {
        sem_wait(&_dataSem); // 申请数据资源,--dataSem

        pthread_mutex_lock(&_cmutex);
        data = _ringQueue[_cIndex];
        ++_cIndex;
        _cIndex %= _capacity;
        cout << "消费者[" << pthread_self() << "]:消费了一个数据:" << data << endl;//便于观察
        pthread_mutex_unlock(&_cmutex);

        sem_post(&_roomSem); // 释放空间资源,++roomSem
    }

private:
    vector<T> _ringQueue;    // 环形队列
    uint32_t _capacity;      // 环形队列大小
    sem_t _roomSem;          // 空间资源个数
    sem_t _dataSem;          // 数据资源个数
    uint32_t _pIndex;        // 表示生产者正在生产的位置
    uint32_t _cIndex;        // 表示消费者正在消费的位置
    pthread_mutex_t _pmutex; // 生产者的锁
    pthread_mutex_t _cmutex; // 消费者的锁
};

RingQueue.cc:

#include "RingQueue.h"

void *pRoutine(void *args)
{
    RingQueue<int> *rq = static_cast<RingQueue<int> *>(args);
    while (1)
    {
        int data = rand() % 1000;
        rq->push(data);
    }
    return nullptr;
}

void *cRoutine(void *args)
{
    RingQueue<int> *rq = static_cast<RingQueue<int> *>(args);
    while (1)
    {
        sleep(2);
        int data = -1;
        rq->pop(data);
    }
    return nullptr;
}
int main()
{
    RingQueue<int> rq(6);
    pthread_t p1,p2,p3, c1,c2,c3,c4;
    pthread_create(&p1, nullptr, pRoutine, &rq);
    pthread_create(&p2, nullptr, pRoutine, &rq);
    pthread_create(&p3, nullptr, pRoutine, &rq);

    pthread_create(&c1, nullptr, cRoutine, &rq);
    pthread_create(&c2, nullptr, cRoutine, &rq);
    pthread_create(&c3, nullptr, cRoutine, &rq);
    pthread_create(&c4, nullptr, cRoutine, &rq);

    pthread_join(p1, nullptr);
    pthread_join(p2, nullptr);
    pthread_join(p3, nullptr);
    pthread_join(c1, nullptr);
    pthread_join(c2, nullptr);
    pthread_join(c3, nullptr);
    pthread_join(c4, nullptr);
    return 0;
}

 运行结果:

 

线程池

应用场景:

需要大量线程完成任务,且完成任务的时间较短

对性能要求苛刻的应用

接受突发性的大量请求

如何实现?

创建固定数量的线程,使用锁和条件变量保证线程同步和互斥,循环从任务队列获取任务对象,执行任务接口。

核心接口:

start:创建指定数量的线程

push:加锁操作任务队列,放入任务后,唤醒在条件下等待的一个线程

pop:直接操作任务队列,取出队头任务(线程安全在线程运行函数保证)

threadRoutine:线程运行函数,加锁判断任务队列是否为空,空则在条件变量下等待唤醒,不为空就取出任务处理,最后解锁

代码实现:

threadPool.hpp:

#include <iostream>
#include <queue>
#include <pthread.h>
#include <sys/prctl.h>
#include <assert.h>
using namespace std;

int gThreadNum = 5;

template <class T>
class ThreadPool
{
private:
    ThreadPool(int threadNum = gThreadNum)
        : threadNum_(threadNum), isStart_(false)
    {
        pthread_mutex_init(&mutex_,nullptr);
        pthread_cond_init(&cond_,nullptr);
    }
    ThreadPool(ThreadPool<T> &) = delete;
    void operator=(ThreadPool<T> &) = delete;

public:
    // 线程安全地创建单例
    static ThreadPool<T> *getInstance()
    {
        if (instance == nullptr) // 过滤掉不满足条件的
        {
            static pthread_mutex_t mutex_s = PTHREAD_MUTEX_INITIALIZER;
            pthread_mutex_lock(&mutex_s);
            if (instance == nullptr)
            {
                instance = new ThreadPool<T>();
            }
            pthread_mutex_unlock(&mutex_s);
        }
        return instance;
    }

    static void *threadRoutine(void *args)
    {
        pthread_detach(pthread_self());
        ThreadPool<T> *tp = static_cast<ThreadPool<T> *>(args);
        prctl(PR_SET_NAME, "follower");
        while(1)
        {
            //用条件变量保证线程间的互斥和同步
            tp->lockQueue();
            while(tp->queueEmpty())
            {
                tp->waitFortask();
            }
            //从任务队列中取出任务;
            T t = tp->pop();
            tp->unlockQueue();

            cout<<"pthread["<<pthread_self()<<"]"<<"取出任务为:"<<t<<endl;
        }
        return nullptr;
    }
    void start()
    {
        assert(!isStart_);
        for (int i = 0; i < threadNum_; i++)
        {
            pthread_t p;
            pthread_create(&p, nullptr, threadRoutine, this); // 传入this,可以访问类内的方法
        }
        isStart_ = true;
    }

    ~ThreadPool()
    {
        pthread_mutex_destroy(&mutex_);
        pthread_cond_destroy(&cond_);
    }

    void push(const T&in)//将任务放入任务队列中
    {
        lockQueue();
        taskQueue_.push(in);
        unlockQueue();

        wakeThread();//唤醒一个线程来处理任务
    }
private:
    void lockQueue(){pthread_mutex_lock(&mutex_);}
    void unlockQueue(){pthread_mutex_unlock(&mutex_);}
    bool queueEmpty(){return taskQueue_.empty();}
    void waitFortask(){pthread_cond_wait(&cond_,&mutex_);}
    void wakeThread(){pthread_cond_signal(&cond_);}//依次唤醒等待队列中的线程
    T pop()//从任务队列中取出任务
    {
        T temp = taskQueue_.front();
        taskQueue_.pop();
        return temp;
    }
private:
    bool isStart_;
    int threadNum_;
    queue<T> taskQueue_;
    pthread_mutex_t mutex_;
    pthread_cond_t cond_;
    // 单例模式
    static ThreadPool<T> *instance;
};

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

threadPoolTest.cc:

#include "threadPool.hpp"
#include <unistd.h>
int main()
{
    ThreadPool<int> *tp = ThreadPool<int>::getInstance();
    tp->start();
    sleep(1);
    srand((unsigned int)time(nullptr));
    while (1)
    {
        tp->push(rand() % 10000);
        sleep(1);
    }
    return 0;
}

输出:

 可以看到线程id顺序是一样的,说明了线程同步。

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

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

相关文章

DHCP中继及配置

为什么需要DHCP Relay&#xff1f;产生背景解决方案DHCP Relay工作原理DHCP Relay配置实现产生背景 随着网络规模的扩大&#xff0c;网络中就会出现用户处于不同网段的情况。 这个时候客户A和客户B要请求IP地址时&#xff0c;首先会发送DHCP Discover广播包&#xff0c;这个广…

一本关于ChatGPT的书《ChatGPT 革命:了解大型语言模型的力量》免费下载

下载地址&#xff1a;https://download.csdn.net/download/winniezhang/87431530 这本书有什么不同&#xff1a; 1、从书名&#xff0c;到大纲&#xff0c;到内容&#xff0c;基本都来自ChatGPT的自述&#xff0c;本人只是负责编辑。 2、整个成书过程只用了2小时不到。 3、…

【排序算法】堆排序(Heap Sort)

堆排序是指利用堆这种数据结构所设计的一种排序算法。堆是一个近似完全二叉树的结构&#xff0c;并同时满足堆积的性质&#xff1a;即子结点的键值或索引总是小于&#xff08;或者大于&#xff09;它的父节点。堆排序介绍学习堆排序之前&#xff0c;有必要了解堆&#xff01;若…

【HTML】HTML 表格 ② ( 表头单元格标签 | 表格标题标签 )

文章目录一、表头单元格标签二、表格标题标签一、表头单元格标签 表头单元格 可以在表格中 用作第一排 作为表格 的 表头 使用 , 表头单元格 中的 文本设置 可以与 普通单元格 中的文本设置 不同 ; 表头单元格 中的 文本 会 居中 , 并且 加粗 显示 ; 表头单元格 标签 如下 : &…

2023彻底解决Typora使用iPic微博图床失效问题

一、问题描述用Typora搭配iPic图床使用&#xff0c;最近csdn图片显示不出来用浏览器打开图片显示403&#xff0c;这里原因是微博图床出问题了导致的而使用iPic其他图床则需要一直付费&#xff0c;那有没有一劳永逸的解决所有问题呢&#xff1f;二、旧图恢复首先怎么找回旧图&am…

数字信号处理-第一章 离散时间信号与系统

1.1 离散时间信号——序列 1.1.1 序列 离散时间信号只在离散时间上给出函数值&#xff0c;是时间上不连续的序列。一 般 &#xff0c;离散时间的间隔是均匀的&#xff0c;以TTT表示&#xff0c;故用 x(nT)x(nT)x(nT)表示此离散时间信号在nTnTnT点上的值&#xff0c;nnn为整数…

省钱的年轻人,钱包被折扣店钻了空子

【潮汐商业评论/原创】过年期间&#xff0c;除了商场超市&#xff0c;小区附近的折扣店成了Amy经常光顾的对象。用Amy的话来说&#xff0c;“跟附近超市比价格&#xff0c;跟大卖场比距离&#xff0c;综合下来折扣店就是我随时购物的不二选择。”从Amy的话里&#xff0c;我们可…

LeetCode-101. 对称二叉树

目录题目分析递归法题目来源 101. 对称二叉树 题目分析 首先想清楚&#xff0c;判断对称二叉树要比较的是哪两个节点&#xff0c;要比较的可不是左右节点&#xff01; 对于二叉树是否对称&#xff0c;要比较的是根节点的左子树与右子树是不是相互翻转的&#xff0c;理解这一…

如何在 Mac 上将 PDF 转换为 Word [6 种免费方法]

我们出于各种原因使用 PDF。我们可以将它们用于学校作业&#xff0c;以打印出谱&#xff0c;或离线阅读文章。我们经常想弄清楚如何在 Mac 上将 Word 文档另存为 PDF。但是&#xff0c;如果我们需要对 PDF 进行更改怎么办&#xff1f; 对 PDF 进行更改通常无法在 PDF 本身上完…

Vue3通透教程【二】更高效的构建工具—Vite

文章目录&#x1f31f; 写在前面&#x1f31f; webpack&#x1f31f; Vite是什么&#xff1f;&#x1f31f; 使用Vite创建项目&#x1f31f; 写在最后&#x1f31f; 写在前面 专栏介绍&#xff1a; 凉哥作为 Vue 的忠实 粉丝输出过大量的 Vue 文章&#xff0c;应粉丝要求开始更…

【成为架构师课程系列】架构师的核心能力地图

目录 架构师核心能力总结 #综合技术能力分层总结 #数据结构和算法知识图谱总结 #Java工程师【核心基础】知识图谱总结

全栈开发工程师面试题五-Eureka

1.服务的提供者和服务的消费者&#xff08;服务的调用关系&#xff09; 服务的提供者&#xff1a;在一次服务中被其他服务调用的服务(提供接口给其他服务) 服务的消费者&#xff1a;一次业务中调用其他服务的服务&#xff08;调用其它微服务提供的接口&#xff09; 服务的提供者…

Koa2-创建、中间件、连接数据库、处理请求、日志

文章目录安装配置koa2配置nodemon,热更新我们的项目中间件什么是中间件&#x1f47b;洋葱模型路由中间件连接数据库 - mysql后端允许跨域处理请求getpostputdelete后续会继续更新安装配置koa2 &#x1f47b;安装 koa2 npm i koa2 -s&#x1f47b;在package.json 配置,当然是在…

如何在QEMU社区提Bug

当在编译及使用QEMU的过程中发现问题又不能自行解决时&#xff0c;可以去QEMU官网提Bug。本文以笔者实际遇到的问题为例&#xff0c;讲解提交QEMU Bug的完整过程。 1. 访问QEMU主页 QEMU官网主页地址为&#xff1a;https://www.qemu.org/ 页面如下&#xff1a; 2. 跳转至CON…

RK3566 多Mipi屏的兼容

需求:项目需求需要同一个固件兼容两款不同IC的mipi屏。以便有一个IC停产之后使用不受影响。由于是同一个模组厂商,所以设计初就要求硬件接口一样或者兼容。 方法:一开始的想法很简单和网上以及同行提供的方法一样在uboot阶段去读屏的ID,然后再把对应屏ID放到cmdline里面去传…

Parasoft全面发布最广泛的MISRA规则覆盖

Parasoft通过其经过市场验证的自动化软件测试工具集成套件&#xff0c;帮助企业持续交付高质量的软件。Parasoft的技术支持嵌入式、企业和物联网市场&#xff0c;通过将静态代码分析和单元测试、Web UI和API测试等所有内容集成到交付管道中&#xff0c;再加上服务虚拟化和完整的…

Python 数据库开发实战 - Python与Redis交互篇- 综合案例 - 新闻管理系统 - 缓存新闻数据至redis

接下来这个章节将继续来完成 《新闻管理系统》 这个项目&#xff0c;上一章节我们完成了 “发表新闻” 这个功能&#xff0c;在发表新闻后&#xff0c;什么时候才会缓存该条新闻记录呢&#xff1f;并不是说在发表新闻成功之后就立刻被缓存&#xff0c;而是该新闻被管理员审批通…

基于Verilog HDL的状态机描述方法

⭐本专栏针对FPGA进行入门学习&#xff0c;从数电中常见的逻辑代数讲起&#xff0c;结合Verilog HDL语言学习与仿真&#xff0c;主要对组合逻辑电路与时序逻辑电路进行分析与设计&#xff0c;对状态机FSM进行剖析与建模。 &#x1f525;文章和代码已归档至【Github仓库&#xf…

配置类的几种写法

需求 通过java配置类实现一个数据库连接池。 以前xml中是这样写的&#xff1a; <!--配置德鲁伊数据库连接池--><bean id"ds" class"com.alibaba.druid.pool.DruidDataSource"><property name"driverClassName" value"${jdbc…

Selenium常用API详解(从入门到进阶)

目录 1、打开页面 2、查找页面元素 3、输入文本 4、点击操作 5、提交操作 6、清除文本 7、获取文本、属性 8、获取页面的标题和URL 9、窗口 9.1、设置窗口大小 9.2、窗口切换 9.2.1、为什么需要窗口切换&#xff1f; 9.2.2、获取句柄的方式 9.2.3、切换句柄 10、…