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

news2025/1/4 21:23:14

文章目录

  • 基于环形队列的生产消费模型
    • 生产者和消费者的关注点
    • 申请和释放资源的问题
    • 规则
  • RingQueue.hpp
    • 单生产者单消费者的生产者消费者模型:
    • 信号量保护环形队列的原理
    • 多生产者多消费者模型+计算任务处理
      • RingQueue.hpp
      • Task.hpp
      • RingQueue.cc

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

环形队列采用数组模拟,用模运算来模拟环状特性, 但是环形结构起始状态和结束状态都是一样的,不好判断为空或者为满, 解决办法:预留一个空的位置


生产者和消费者的关注点

对于生产者和消费者来说,它们关注的资源是不同的

生产者关注的是空间资源,消费者关注的是数据资源

  • 生产者关注的是环形队列当中是否有空间(blank),只要有空间生产者就可以进行生产
  • 消费者关注的是环形队列当中是否有数据(data),只要有数据消费者就可以进行消费

关于信号量blank_semdata_sem的初始值设置

现在我们用信号量来描述环形队列当中的空间资源(blank_sem)和数据资源(data_sem),在我们初始信号量时给它们设置的初始值是不同的:

  • blank_sem的初始值我们应该设置为环形队列的容量,因为刚开始时环形队列当中全是空间
  • data_sem的初始值我们应该设置为0,因为刚开始时环形队列当中没有数据

申请和释放资源的问题

**对于生产者而言:**生产者申请空间资源,释放数据资源

  • 对于生产者来说,生产者每次生产数据前都需要先申请对于的信号量blank_sem

    • 如果blank_sem的值不为0,则信号量申请成功,此时生产者可以进行生产操作
    • 如果blank_sem的值为0,则信号量申请失败,此时生产者需要在blank_sem的等待队列下进行阻塞等待,直到环形队列当中有新的空间后再被唤醒
  • 当生产者生产完数据后,应该释放对于的空间资源->data_sem:

    • 虽然生产者在进行生产前是对blank_sem进行的P操作,但是当生产者生产完数据,应该对data_sem进行V操作而不是blank_sem
    • 生产者在生产数据前申请到的是blank位置,当生产者生产完数据后,该位置当中存储的是生产者生产的数据,在该数据被消费者消费之前,该位置不再是blank位置,而应该是data位置
    • 所以当生产者生产完数据后,意味着环形队列当中多了一个data位置,因此我们应该对data_sem进行V操作

**对于消费者而言:**消费者申请数据资源,释放空间资源

对于消费者来说,消费者每次消费数据前都需要先申请data_sem:

  • 如果data_sem的值不为0,则信号量申请成功,此时消费者可以进行消费操作
  • 如果data_sem的值为0,则信号量申请失败,此时消费者需要在data_sem的等待队列下进行阻塞等待,直到环形队列当中有新的数据后再被唤醒

当消费者消费完数据后,应该释放blank_sem:

  • 虽然消费者在进行消费前是对data_sem进行的P操作,但是当消费者消费完数据,应该对blank_sem进行V操作而不是data_sem
  • 消费者在消费数据前申请到的是data位置,当消费者消费完数据后,该位置当中的数据已经被消费过了,再次被消费就没有意义了,为了让生产者后续可以在该位置生产新的数据,我们应该将该位置算作blank位置,而不是data位置
  • 当消费者消费完数据后,意味着环形队列当中多了一个blank位置,因此我们应该对blank_sem进行V操作

规则

生产者和消费者必须遵守如下两个规则 规则1)生产者和消费者不能对同一个位置进行访问

生产者和消费者在访问环形队列时:

  • 如果生产者和消费者访问的是环形队列当中的同一个位置,那么此时生产者和消费者就相当于同时对这一块临界资源进行了访问,这当然是不允许的
  • 如果生产者和消费者访问的是环形队列当中的不同位置,那么此时生产者和消费者是可以同时进行生产和消费的,此时不会出现数据不一致等问题

image-20220829212716686


规则2)无论是生产者还是消费者,都不应该将对方套一个圈以上

  • 生产者从消费者的位置开始一直按顺时针方向进行生产,如果生产者生产的速度比消费者消费的速度快,那么当生产者绕着消费者生产了一圈数据后再次遇到消费者,此时生产者就不应该再继续生产了,因为再生产就会覆盖还未被消费者消费的数据
  • 同理,消费者从生产者的位置开始一直按顺时针方向进行消费,如果消费者消费的速度比生产者生产的速度快,那么当消费者绕着生产者消费了一圈数据后再次遇到生产者,此时消费者就不应该再继续消费了,因为再消费就会消费到缓冲区中保存的废弃数据

RingQueue.hpp

我们用数组实现环形队列RingQueue,其中这个环形队列就是生产者消费者模型当中的交易场所

注意事项:

  • 当不设置环形队列的大小时,我们默认将环形队列的容量上限设置为10
  • 代码中的RingQueue是用vector实现的,生产者每次生产的数据放到vector下标为p_pos的位置,消费者每次消费的数据来源于vector下标为c_pos的位置
  • 生产者每次生产数据后p_pos都会进行++,标记下一次生产数据的存放位置,++后的下标会与环形队列的容量进行取模运算,实现“环形”的效果
  • 消费者每次消费数据后c_pos都会进行++,标记下一次消费数据的来源位置,++后的下标会与环形队列的容量进行取模运算,实现“环形”的效果
  • p_pos只会由生产者线程进行更新,c_pos只会由消费者线程进行更新,对这两个变量访问时不需要进行保护,因此代码中将p_pos和c_pos的更新放到了V操作之后,就是为了尽量减少临界区的代码
#pragma once
#include <iostream>
#include <vector>
#include <semaphore.h>

namespace Mango
{
    const int cap_default = 10;
    template<class T>
    class RingQueue
    {
public:
        RingQueue(int cap = cap_default)
            :_cap(cap),c_step(0),p_step(0)
        {
            _ringqueue.resize(cap);
            //初始化信号量,第三个参数:信号量的初始值
            sem_init(&blank_sem, 0, cap);//最初有cap个空格子资源,所以blank_sem初始值设置为环形队列的容量
            sem_init(&data_sem, 0, 0);//最初没有数据,数据资源为0
        }
        ~RingQueue()
        {
            sem_destroy(&blank_sem);
            sem_destroy(&data_sem);
        }

        //向环形队列插入数据->生产者使用
        void Push(const T& in)
        {
            //step1:申请信号量
            sem_wait(&blank_sem);//生产者要申请空位置的资源,P操作(空位置)
            //step2:在当前p_step位置生产(放数据)
            _ringqueue[p_step] = in;
            //step3:多了一个数据->数据资源增加(V操作(数据资源))
            sem_post(&data_sem);
            //step4:处理p_step的位置,防止越界
            p_step++;
            p_step %=_cap;
        }

        //从环形队列获取数据->消费者使用
        void Pop(T* out)//输出型参数
        {
            //step1:申请信号量
             sem_wait(&data_sem);  //消费者要申请数据资源,P操作(数据资源)
            //step2:取出在c_step位置的数据(拿数据)
            *out = _ringqueue[c_step];
            //step3:少了一个数据->空间资源增加(V操作(空间资源))
            sem_post(&blank_sem);
            //step4:处理c_step的位置,防止越界
            c_step++;
            c_step %= _cap;
        }
private:
            std::vector<T> _ringqueue;//数组模拟实现环形队列
            int _cap;//记录容量
            sem_t blank_sem;//描述空间资源->生产者关心空位置资源
            sem_t data_sem;//描述数据资源->消费者关心数据资源
            int c_step;//消费者当前所在的位置
            int p_step;//生产者当前所在的位置
    };
}

单生产者单消费者的生产者消费者模型:

主函数我们就只需要创建一个生产者线程和一个消费者线程,生产者线程不断生产数据放入环形队列,消费者线程不断从环形队列里取出数据进行消费

注意事项:

  • 环形队列要让生产者线程向队列中Push数据,让消费者线程从队列中Pop数据,因此这个环形队列必须要让这两个线程同时看到,所以我们在创建生产者线程和消费者线程时,需要将环形队列作为线程执行例程的参数进行传入
  • 代码中生产者生产数据就是将获取到的随机数Push到环形队列,而消费者就是从环形队列Pop数据,为了便于观察,我们可以将生产者生产的数据和消费者消费的数据进行打印输出
#include "RingQueue.hpp"
#include <pthread.h>
#include <time.h>
#include <unistd.h>

using namespace Mango;

void* consumer(void* args)
{
    RingQueue<int>* rq  =  ( RingQueue<int>*)args;
    while(1)
    {
        sleep(1);
        int data = 0;
        rq->Pop(&data);//输出型参数
        std::cout << "消费数据是: " << data << std::endl;
    }
}
void* producter(void* args)
{
    RingQueue<int>* rq  =  ( RingQueue<int>*)args;
    while(1)
    {
        sleep(1);
        int data = rand()%20 +1;//1~20的数据
        std::cout << "生产数据是:  " << data << std::endl;
        rq->Push(data);
    }
}
int main()
{
    //单生产单消费模型
    srand((long long)time(0));
    
    RingQueue<int>* rq = new RingQueue<int>();

    pthread_t c,p;
    //把环形队列以参数的形式传给两个线程,相当于两个线程有了进行通信的数据区域
    pthread_create(&c, nullptr, consumer, (void*)rq);
    pthread_create(&p, nullptr, producter, (void*)rq);

    pthread_join(c, nullptr);
    pthread_join(p, nullptr);
    return 0;
}

生产者和消费者生产速度一致时: 此时生产者和消费者的执行步调是一致的

image-20220829230646639

生产者生产的快,消费者消费的慢的情况: 让生产者不停的进行生产,而消费者每隔一秒进行消费,

此时可以看到:由于生产者生产的很快,运行代码后一瞬间生产者就将环形队列打满了,此时生产者想要再进行生产,但空间资源已经为0了,于是生产者只能在blank_sem的等待队列下进行阻塞等待,直到由消费者消费完一个数据后对blank_sem进行了V操作,生产者才会被唤醒进而继续进行生产

但由于生产者的生产速度很快,生产者生产完一个数据后又会进行等待,因此后续生产者和消费者的步调又变成一致的了

image-20220829230820161

生产者生产的慢,消费者消费的快 : 让生产者每隔一秒进行生产,而消费者不停的进行消费

image-20220829230945696

虽然消费者消费的很快,但一开始环形队列当中的数据资源为0,因此消费者只能在data_sem的等待队列下进行阻塞等待,直到生产者生产完一个数据后对data_sem进行了V操作,消费者才会被唤醒进而进行消费.

但由于消费者的消费速度很快,消费者消费完一个数据后又会进行等待,因此后续生产者和消费者的步调又变成一致的了


信号量保护环形队列的原理

在blank_sem和data_sem两个信号量的保护后,该环形队列中不可能会出现数据不一致的问题.

因为只有当生产者和消费者指向同一个位置并访问时,才会导致数据不一致的问题,而此时生产者和消费者在对环形队列进行写入或读取数据时,只有两种情况会指向同一个位置:

  • 环形队列为空时
  • 环形队列为满时

但是在这两种情况下,生产者和消费者不会同时对环形队列进行访问:

  • 当环形队列为空的时,消费者一定不能进行消费,因为此时数据资源为0
  • 当环形队列为满的时,生产者一定不能进行生产,因为此时空间资源为0

也就是说,当环形队列为空和满时,我们已经通过信号量保证了生产者和消费者的串行化过程

而除了这两种情况之外,生产者和消费者指向的都不是同一个位置,因此该环形队列当中不可能会出现数据不一致的问题

并且大部分情况下生产者和消费者指向并不是同一个位置,因此大部分情况下该环形队列可以让生产者和消费者并发的执行


多生产者多消费者模型+计算任务处理

RingQueue.hpp

由于现在是多执行流,所以c_step和p_step可能会被多个执行流访问,是临界资源,我们还要维护生产者和生产者的互斥,消费者和消费者的互斥.所以我们需要定义两把互斥锁分别进行保护!

要访问临界资源之前,先加锁

问:加锁应该在申请信号量之前还是在申请信号量之后?

信号量的P操作本身就是原子的,如果放在前面,那么就只有申请锁成功的人才能申请信号量,即使信号量设置的非常高,但实际上申请的时候还是要一个一个的申请, 不是我们想要的效果,放在前面和单生产者单消费者没有本质差别

所以推荐放在后面,放在后面有一个好处:可以保证进入生产区域+更新下标的时候是只有一个执行流进入的,此时有资格竞争锁的前提,必须得先申请信号量,如果有多个生产者,相当于就可以预先把信号量分配给多个对于的生产者 ,当锁一旦释放了,其它人立马申请锁,

即:在你申请锁成功访问临界资源的时候,别人就可以并行的把信号量先准备好


多生产和多消费的优势不在于拿数据和放数据,而在于并发的获取和处理任务

#pragma once
#include <iostream>
#include <vector>
#include<pthread.h>
#include <semaphore.h>

namespace Mango
{
    const int cap_default = 10;
    template<class T>
    class RingQueue
    {
        public:
        RingQueue(int cap = cap_default)
            :_cap(cap),c_step(0),p_step(0)
        {
            _ringqueue.resize(cap);
            //初始化信号量,第三个参数:信号量的初始值
            sem_init(&blank_sem, 0, cap);//最初有cap个空格子资源
            sem_init(&data_sem, 0, 0);//最初没有数据,数据资源为0

            //初始化互斥锁
            pthread_mutex_init(&c_mtx_, nullptr);
            pthread_mutex_init(&p_mtx_, nullptr);
        }
        ~RingQueue()
        {
            sem_destroy(&blank_sem);
            sem_destroy(&data_sem);
            
            //释放互斥锁
            pthread_mutex_destroy(&c_mtx_);
            pthread_mutex_destroy(&p_mtx_);
        }

        //插入数据->生产者使用
        void Push(const T& in)
        {
            //step1:申请信号量
            sem_wait(&blank_sem);//生产者要申请空位置的资源,P操作(空位置)
            //访问临界资源前先加锁:
            pthread_mutex_lock(&p_mtx_);
            //step2:在当前p_step位置生产(放数据)
            _ringqueue[p_step] = in;
            //step3:多了一个数据->数据资源增加(V操作(数据资源))
            sem_post(&data_sem);
            //step4:处理p_step的位置,防止越界
            p_step++;
            p_step %=_cap;
            pthread_mutex_unlock(&p_mtx_);//解锁
        }

        //弹出数据->消费者使用
        void Pop(T* out)//输出型参数
        {
            //step1:申请信号量
             sem_wait(&data_sem);  //消费者要申请数据资源,P操作(数据资源)
            //访问临界资源前先加锁:
            pthread_mutex_lock(&c_mtx_);
            //step2:取出在c_step位置的数据(拿数据)
            *out = _ringqueue[c_step];
            //step3:少了一个数据->空间资源增加(V操作(空间资源))
            sem_post(&blank_sem);
            //step4:处理c_step的位置,防止越界
            c_step++;
            c_step %= _cap;
            pthread_mutex_unlock(&c_mtx_);
        }
        private:
            std::vector<T> _ringqueue;//数组模拟实现环形队列
            int _cap;//记录容量
            sem_t blank_sem;//描述空间资源->生产者关心空位置资源
            sem_t data_sem;//描述数据资源->消费者关心数据资源
            int c_step;//消费者当前所在的位置
            int p_step;//生产者当前所在的位置

            //新增:两把锁,保护临界资源
            pthread_mutex_t c_mtx_;
            pthread_mutex_t p_mtx_;
    };
}

上述代码,我们分开的使用两把锁,就维护了生产者和生产者的互斥关系,消费者和消费者的互斥关系,他们两套互斥互不影响,任意时刻最多只允许有一个生产者执行流进入生产,最多只允许有一个消费者执行流进入消费,每一个能进去的人,前提是申请信号量成功,他们进入临界资源的生产和消费的同步和互斥由信号量保证


实际使用生产者消费者模型时可不是简单的让生产者生产一个数字让消费者进行打印而已,我们这样做只是为了测试代码的正确性,由于我们将RingQueue当中存储的数据进行了模板化,此时就可以让RingQueue当中存储其他类型的数据

Task.hpp

例如:

实现一个基于计算任务的生产者消费者模型,此时我们只需要定义一个Task类,这个类当中需要包含一个Run成员函数,该函数代表着我们想让消费者如何处理拿到的数据

#pragma once

#include <iostream>
#include <pthread.h>

namespace Mango
{
    class Task
    {
    private:
        int x_;
        int y_;
        char op_; //+/*/%
    public:
        // void (*callback)();
        Task() {}
        Task(int x, int y, char op) : x_(x), y_(y), op_(op)
        {
        }
        
        std::string Show()
        {
            std::string message = std::to_string(x_);
            message += op_;
            message += std::to_string(y_);
            message += "=?";
            return message;
        }
        
        int Run()
        {
            int res = 0;
            switch (op_)
            {
            case '+':
                res = x_ + y_;
                break;
            case '-':
                res = x_ - y_;
                break;
            case '*':
                res = x_ * y_;
                break;
            case '/':
                res = x_ / y_;
                break;
            case '%':
                res = x_ % y_;
                break;
            default:
                std::cout << "bug??" << std::endl;
                break;
            }
            std::cout << "当前任务正在被线程: " << pthread_self() << " 处理: " \
            << x_ << op_ << y_ << "=" << res << std::endl;
            return res;
        }
        int operator()()	//仿函数
        {
            return Run();
        }
        ~Task() {}
    };
}

这里我们重载了operator , 定义一个对象Task t; t()本质相当于:t.operator() ->调用Run函数, 这种做法和t.Run()没有本质区别


此时生产者放入环形队列的数据就是一个Task对象,而消费者从环形队列拿到Task对象后,就可以用该对象调用Run成员函数进行数据处理

RingQueue.cc

我们让生产者慢一点,观察现象

#include "RingQueue.hpp"
#include"Task.hpp"
#include <pthread.h>
#include <time.h>
#include <unistd.h>

using namespace Mango;

void* consumer(void* args)
{
    RingQueue<Task>* rq = (RingQueue<Task>*)args;
     while(true)
     {       
         Task t;
         rq->Pop(&t);
         t(); //相当于t.operator()  ->t.Run()
         sleep(1);
     }
}
void* producter(void* args)
{
    RingQueue<Task>* rq = (RingQueue<Task>*)args;
    const std::string ops = "+-*/%";
    while(true)
    {
        sleep(1);
        int x = rand()%20 + 1;
        int y = rand()%10 + 1;
        char op = ops[rand()%ops.size()];
        Task t(x, y, op);
        std::cout << "生产数据是:  " << t.Show() << "当前线程是: " << pthread_self()<< std::endl;
        rq->Push(t);
    }
}
int main()
{
    //改成多生产者多消费者模型
    srand((long long)time(nullptr));
    RingQueue<Task>* rq = new RingQueue<Task>();

    pthread_t c0,c1,c2,c3,p0,p1,p2;

    pthread_create(&c0, nullptr, consumer, (void*)rq);
    pthread_create(&c1, nullptr, consumer, (void*)rq);
    pthread_create(&c2, nullptr, consumer, (void*)rq);
    pthread_create(&c3, nullptr, consumer, (void*)rq);
    pthread_create(&p0, nullptr, producter, (void*)rq);
    pthread_create(&p1, nullptr, producter, (void*)rq);
    pthread_create(&p2, nullptr, producter, (void*)rq);

    pthread_join(c0, nullptr);
    pthread_join(c1, nullptr);
    pthread_join(c2, nullptr);
    pthread_join(c3, nullptr);
    pthread_join(p0, nullptr);
    pthread_join(p1, nullptr);
    pthread_join(p2, nullptr);
    return 0;
}

image-20220829235222734


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

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

相关文章

安装pytourch gpu并测试

输入nvidia-smi命令查看cuda版本号, 系统的CUDA版本决定了系统最高可以支持什么版本的cudatoolkit&#xff0c;它是向下兼容的, 可以装低版本但是不能装高版本。 更新下conda&#xff0c;用管理员打开cmd conda update -n base -c defaults conda 安装CUDATookit 使用以下命令…

8 指数族分布【手写+Xmind笔记】

文章目录 8 指数族分布【手写Xmind笔记】8.1 Xmind笔记8.2 手写证明 8 指数族分布【手写Xmind笔记】 8.1 Xmind笔记 8.2 手写证明

第十二篇、基于Arduino uno,获取多个按键的输入信号(滤波消抖)——结果导向

0、结果 说明&#xff1a;先来看看串口调试助手显示的结果&#xff0c;当按下按键的时候&#xff0c;按一次会打印一次按键被按下&#xff0c;并且打印是哪个按键被按下。如果是你想要的&#xff0c;可以接着往下看。 1、外观 说明&#xff1a;虽然每个型号的按键形态各异&a…

Linux---用户的权限

专栏&#xff1a;Linux 个人主页&#xff1a;HaiFan. 本章为大家带来用户的权限的讲解 用户的权限 Linux权限的概念权限的三类对象权限的三种类型权限设置chmod/chown/chgrp更改权限chmodchownchgrp umask目录的权限粘滞位 Linux权限的概念 Linux下有两种用户&#xff1a;超级…

安科瑞应急照明的环境适用性

安科瑞 徐浩竣 江苏安科瑞电器制造有限公司 zx acrelxhj 摘要&#xff1a;论述消防应急照明和疏散指示系统在实际工程应用过程中系统产品选型、设置及维护环节普遍存在的问题&#xff0c;并提出相应的解决对策。 关键词&#xff1a;应急照明疏散指示产品选型环境适用性灯具…

机器学习笔记 - 使用稳定扩散模型创建图像

一、简述 文本到图像生成是机器学习 (ML) 模型从文本描述生成图像的任务。目标是生成与描述非常匹配的图像,捕捉文本的细节和细微差别。这项任务具有挑战性,因为它要求模型理解文本的语义和语法,并生成逼真的图像。文本到图像生成在 AI 摄影、概念艺术、建筑建筑、时尚、视…

用JMeter自动化测试实现高效稳定的接口测试方案!

目录 前言&#xff1a; 1. 接口与接口测试 1.1 接口概述 1.2 接口测试 2. 基于JMeter的接口测试 2.1 JMeter概述 2.2 用JMeter实现接口测试 3. 基于JMeter的接口自动化测试 3.1 接口自动化测试基础 3.2 使用JMeter进行接口自动化测试 总结 前言&#xff1a; 接口是…

(字符串) 541. 反转字符串 II——【Leetcode每日一题】

❓541. 反转字符串 II 难度&#xff1a;简单 给定一个字符串 s 和一个整数 k&#xff0c;从字符串开头算起&#xff0c;每计数至 2k 个字符&#xff0c;就反转这 2k 字符中的前 k 个字符。 如果剩余字符少于 k 个&#xff0c;则将剩余字符全部反转。如果剩余字符小于 2k 但大…

【dns awsl】RedHat8配置DNS服务器

文章目录 一、安装 配置DNS验证 一、安装 配置DNS 安装 sudo dnf install bind bind-utils配置named.conf 指定正向解析和反向解析的所需文件 vim /etc/named.conf在文件中找到options部分&#xff0c;并进行如下修改&#xff1a;options {listen-on port 53 { any; };allow…

day02——特征工程之特征提取

特征工程之特征提取 一、什么是特征工程二、特征提取1&#xff0c;字典特征提取2&#xff0c;文本特征提取&#xff08;1&#xff09;英文文本特征提取&#xff08;2&#xff09;中文文本特征提取&#xff08;3&#xff09;Tf-idf 文本特征提取 一、什么是特征工程 特征工程是…

CRM和SCRM有什么区别?

首先讲下CRM是什么&#xff1f;光是概念就有7个之多&#xff1a; 提出者概念GartnerGroupCRM是一种商业策略&#xff0c;它按照客户的分类情况有效地组织企业资源&#xff0c;培养以客户为中心的经营行为以及实施以客户为中心的业务流程&#xff0c;并以此为手段来提高企业的赢…

低代码平台安全性探究:解析低代码平台的安全性及应对措施

近年来&#xff0c;低代码平台由于能够以最少的代码快速开发应用程序而变得越来越流行。然而&#xff0c;随着数据泄露和网络威胁的增加&#xff0c;企业有理由质疑低码平台是否安全。在本文中&#xff0c;我们将探讨低代码平台安全吗&#xff1f; 一、低码平台如何工作。 在我…

集成websocket实现实时通信(ruoyi 使用笔记)

集成websocket实现实时通信(ruoyi 使用笔记 1.简单介绍WebSocket2.详细代码2.1WebSocketConfig2.2 SemaphoreUtils2.3 WebSocketServer2.4 WebSocketUsers2.5 html2.6 vue版本前端代码2.7 controller 1.简单介绍WebSocket Websocket 是一种基于 TCP 协议的全双工通信协议&#…

加油站“变身”快充站,探讨充电新模式

摘要&#xff1a;新能源汽车规模化发展的同时&#xff0c;充电不便利的痛点愈发明显。在未来的新能源汽车行业发展当中&#xff0c;充电的矛盾要远远大于造车的矛盾&#xff0c;解决好充电的问题成为电动汽车行业发展的一个突出问题。解决充电补能问题&#xff0c;重要的方式之…

自动化测试还是手动测试?深度探讨Web自动化测试的利与弊,精准性和可靠性抉择应如何。

目录 前言&#xff1a; 1. 自动化测试的价值 2. 自动化测试的瓶颈 总结 前言&#xff1a; 随着互联网的飞速发展&#xff0c;Web应用越来越成为我们日常工作和生活中必不可少的一部分。这也就意味着&#xff0c;Web应用的质量和稳定性变得至关重要。而Web自动化测试作为保…

87.建立主体页面-第三部分

上节我们完成的页面如下&#xff1a; ● 我们预计在按钮下面放置一些用户案例 去年我们送了25万多份餐品! ![在这里插入图片描述](https://img-blog.csdnimg.cn/c71d57199b834a8c9763a345939adc5d.png) ● 我们将这些图片文字以flex布局方式排列摆放 .delivered-meals {dis…

ebpf代码编写小技巧

查看所有tracepoint perf list perf追踪tracepoint perf trace --no-syscalls --event net:*查看tracepoint的具体参数 sudo python3 /usr/share/bcc/tools/tplist -v net:napi_gro_receive_entry cat /sys/kernel/debug/tracing/events/net/netif_rx/format内核vmlinux.h生…

Zinx框架学习 - 链接封装与业务绑定

Zinx - V0.2 链接封装与业务绑定 之前的v0.1版本&#xff0c;已经实现了一个基础的Server框架&#xff0c;现在需要对客户端链接和不同的客户端链接锁处理的不同业务再做一层接口封装在ziface下创建一个属于链接的接口文件iconnection.go&#xff0c;znet下创建文件connection…

异步利刃CompletableFuture

什么是CompletableFuture? CompletableFuture 类实现了 Future 和 CompletionStage 接口并且新增了许多方法&#xff0c;它支持 lambda&#xff0c;通过回调利用非阻塞方法&#xff0c;提升了异步编程模型。简单来说可以帮我们实现任务编排。【文中所有代码已上传码云】 Com…

程序员必修必炼的设计模式之工厂模式

本文首发自「慕课网」&#xff08;www.imooc.com&#xff09;&#xff0c;想了解更多IT干货内容&#xff0c;程序员圈内热闻&#xff0c;欢迎关注"慕课网"或慕课网公众号&#xff01; 作者&#xff1a;李一鸣 | 慕课网讲师 工厂模式是平时开发过程中最常见的设计模式…