Sylar C++高性能服务器学习记录09 【协程调度模块-知识储备篇】

早在19年5月就在某站上看到sylar的视频了,一直认为这是一个非常不错的视频,由于本人一直是自学编程,基础不扎实,也没有任何人的督促,没能坚持下去,每每想起倍感惋惜。恰逢互联网寒冬,在家无事,遂提笔再续前缘。

为了能更好的看懂sylar,本套笔记会分两步走,每个系统都会分为两篇博客。
分别是【知识储备篇】和【代码分析篇】
(ps:纯粹做笔记的形式给自己记录下,欢迎大家评论,不足之处请多多赐教)
QQ交流群:957100923


协程调度模块-代码分析篇

一、Schedule调度器是什么【线程级别的调度器】

我的博客已经停更一周了,并不是 五一 假期出去游玩导致的。
原因很简单:就是因为 sylar 视频中协程调度相关的知识过于复杂,导致自己反复观看了数遍才勉强理解一二。
要搞懂协程调度模块一上来就看sylar的视频或代码未免门槛太高了。
我总结了一下这部分知识为什么难以下咽

  1. 知识点过多,将调度器、线程、协程糅合在一起讲解,原本基础就不扎实,这样一锅乱炖容易糊掉。
  2. 想法没有提前告知,不知道要做什么,在做什么,雾里看花。
  3. 弹幕的误导,sylar-yin 确实会有说错写错的地方,如果你不懂,那么请你百分百遵循他的写法。
  4. 不够坚持,不要放过每一个你不熟悉的知识点,请暂停视频,把不会的知识点了解了再往下(不要过分深入)。

本人属于不太聪明的类型,所以如果你和我一样,那么请稳扎稳打,别人学一天我们就学三天,别人学一个月,我们就学一年。


1.最普通的多线程使用

首先抛开协程,我们想要使用多线程来完成多个任务需要怎么做呢?
下面我举个例子(该案例中会开启三个子线程来分别执行 唱歌、跳舞、说唱):

#include <chrono>
#include <iostream>
#include <pthread.h>
#include <vector>
#include <list>
#include <unistd.h>
#include <functional>
#include "sylar/thread.h"
#include "sylar/mutex.h"

//测试用的全局累加计数字段
int num = 0;
//循环累加的次数
uint64_t loop_times = 100000000;

typedef sylar::Mutex MutexType;
MutexType g_mutx;

//唱歌的方法,会专门分配一个线程来处理
void sing(){
    MutexType::Lock lock(g_mutx);
    for(size_t i = 0; i < loop_times; ++i){
        ++num;
    }
    std::cout << sylar::Thread::GetName() << " sing~ " << num << std::endl;
}

//跳舞的方法,会专门分配一个线程来处理
void dance(){
    MutexType::Lock lock(g_mutx);
    for(size_t i = 0; i < loop_times; ++i){
        ++num;
    }
    std::cout <<sylar::Thread::GetName() << " dance~ " << num << std::endl;   
}

//说唱的方法,会专门分配一个线程来处理
void rap(){
    MutexType::Lock lock(g_mutx);
    for(size_t i = 0; i < loop_times; ++i){
        ++num;
    }
    std::cout <<sylar::Thread::GetName() << " rap~ " << num << std::endl;   
}


int main(){
    std::cout << "====Main start====" << std::endl;
    {
        //记录当前时间
        auto start = std::chrono::high_resolution_clock::now();

        //创建线程用于执行唱歌的方法
        sylar::Thread::ptr thr1(new sylar::Thread(&sing,"THREAD_1"));
        //创建线程用于执行跳舞的方法
        sylar::Thread::ptr thr2(new sylar::Thread(&dance,"THREAD_2"));
        //创建线程用于执行说唱的方法
        sylar::Thread::ptr thr3(new sylar::Thread(&rap,"THREAD_3"));

        thr1->join();
        thr2->join();
        thr3->join();
        
        //记录结束时间
        auto end = std::chrono::high_resolution_clock::now();
        //计算花费的时间
        std::chrono::duration<double,std::milli> elapsed = end - start;
        std::cout << "Elapsed: " << (int)elapsed.count() << std::endl;

    }

    std::cout << "====Main end====" << std::endl;

    return 0;
}

以下是输出(可以看到三个线程按顺序执行了):

===Main start===
THREAD_1 sing~ 100000000
THREAD_2 dance~ 200000000
THREAD_3 rap~ 300000000
Elapsed: 294
===Main end====

2.复用多线程

上述的代码中,我们有三个任务(唱歌、跳舞、说唱),同时我们开辟了三个线程来执行。
但是很多情况下我们的线程数量是需要控制在适当范围内的。

最佳线程数目 = ((线程等待时间+线程CPU时间)/线程CPU时间 )* CPU数目

CPU核数为4核,一个任务线程cpu耗时为20ms,线程等待(网络IO、磁盘IO)耗时80ms,
那最佳线程数目:( 80 + 20 )/20 * 4 = 20。也就是设置20个线程数最佳。

线程的等待时间越大,线程数就要设置越大
1、CPU密集型:操作内存处理的业务,一般线程数设置为:CPU核数 + 1 或者 CPU核数*2。核数为4的话,一般设置 5 或 8
2、IO密集型:文件操作,网络操作,数据库操作,一般线程设置为:cpu核数 / (1-0.9),核数为4的话,一般设置 40

我的机器是1核的,我这里其实适合使用两个线程,但是我在上述代码中使用了三个线程,那么多线程反而变成了低效的。
所以我们可以复用其中一个线程,在该线程执行完成任务后再执行其他的任务。
代码如下(改动很少,改动部分用 !!!标注了):

int main(){
    std::cout << "====Main start====" << std::endl;
    {
        //记录当前时间
        auto start = std::chrono::high_resolution_clock::now();

        //创建线程用于执行唱歌的方法
        sylar::Thread::ptr thr1(new sylar::Thread(&sing,"THREAD_1"));
        //创建线程用于执行跳舞的方法
        sylar::Thread::ptr thr2(new sylar::Thread(&dance,"THREAD_2"));
        thr1->join();
        thr2->join();

        //!!!复用线程用于执行说唱的方法!!!
        thr1.reset(new sylar::Thread(&rap,"THREAD_1"));
        thr1->join();
        
        //记录结束时间
        auto end = std::chrono::high_resolution_clock::now();
        //计算花费的时间
        std::chrono::duration<double,std::milli> elapsed = end - start;
        std::cout << "Elapsed: " << (int)elapsed.count() << std::endl;

    }

    std::cout << "====Main end====" << std::endl;

    return 0;
}

以下是输出:

===Main start====
THREAD_1 sing~ 100000000
THREAD_2 dance~ 200000000
THREAD_1 rap~ 300000000
Elapsed: 265
==Main end====

问题:
问题来了,以上的两份代码都存在以下问题:
1.就是需要程序员自己来控制线程的创建。
2.需要程序员手动来进行线程的调度。
3.需要程序员手动的去复用一个线程对象。
4.需要程序员手动的join对应的每一个线程。
这样是很繁琐的事情,而且一不小心就会出错,那么我们有什么好的办法吗?


3.调度器的出现

想要解决以上代码带来的问题,我们就要提出需求:
1.将创建线程的事情交由【调度器】执行,我们只需创建调度器时指定线程数量即可。
2.将线程调度和复用线程的事情交由【调度器】执行,我们只需提供所需执行的 任务 即可。
3.等待各个线程完成的jion()方法也由【调度器】执行,我们只需等待调度器结束即可。

综上所述,我们给出以下代码(记得先看main方法中的调度器使用,再看实现):

#include <chrono>
#include <iostream>
#include <pthread.h>
#include <vector>
#include <list>
#include <unistd.h>
#include <functional>
#include "sylar/thread.h"
#include "sylar/mutex.h"

//测试用的全局累加计数字段
int num = 0;
//循环累加的次数
uint64_t loop_times = 100000000;

//唱歌的方法,会专门分配一个线程来处理
void sing(){
    for(size_t i = 0; i < loop_times; ++i){
        ++num;
    }
    std::cout << sylar::Thread::GetName() << " sing~ " << num << std::endl;
}

//跳舞的方法,会专门分配一个线程来处理
void dance(){
    for(size_t i = 0; i < loop_times; ++i){
        ++num;
    }
    std::cout <<sylar::Thread::GetName() << " dance~ " << num << std::endl;   
}

//说唱的方法,会专门分配一个线程来处理
void rap(){
    for(size_t i = 0; i < loop_times; ++i){
        ++num;
    }
    std::cout <<sylar::Thread::GetName() << " rap~ " << num << std::endl;   
}

//调度器
class Schedule {
public:
    //定义锁,使用typedef方便锁类型替换
    typedef sylar::Mutex MutexType;

    //调度器构造函数,指定线程池的大小
    Schedule(size_t size = 1)
        :m_poll_size(size){
        std::cout << "Specify a thread poll size of "<< size << std::endl;
    }
    
    //调度器析构函数
    ~Schedule(){
    
    }

    //调度方法
    void run(){
        //多个线程操作任务队列,涉及到线程安全,所以要加锁
        MutexType::Lock lock(m_mutex);
        //定义一个任务对象来存放后续从任务队列中取出的任务
        std::function<void()> task;
        //开始对任务队列进行迭代
        auto it = m_tasks_queue.begin();
        while(it != m_tasks_queue.end()){
            //将任务从任务队列中取出
            task = *it;
            //将取出的任务从任务队列中擦除
            m_tasks_queue.erase(it);
            //每次按顺序取出一个,取出后跳出循环
            break;
        }
        //判断是否有取到任务
        if(task != nullptr){
            //有取到任务则执行任务
            task();
        }
    }

    //开启调度器方法
    void start(){
        std::cout << "Schedule start..." << std::endl;
        std::cout << "Init a thread poll" << std::endl;
        //根据调度器初始化时指定的线程池大小,来初始化线程池容器的大小 
        m_thread_poll.resize(m_poll_size);
        std::cout << "Schedule run..." << std::endl;
        //构建线程池中的每一个线程对象,每个线程都将绑定调度器的run方法,且给每个线程对象指定了名称用于区分
        for(size_t i = 0; i < m_poll_size; ++i){
            //这里的reset方法是为了复用线程对象节约内存资源
            m_thread_poll[i].reset(new sylar::Thread(std::bind(&Schedule::run,this),"THREAD_"+std::to_string(i)));
        }
    }
    
    //停止调度器方法
    void stop(){
        //判断任务队列中的任务是否全部被消化
        while(!m_tasks_queue.empty()){
            for(size_t i = 0; i < m_poll_size; ++i){
                m_thread_poll[i].reset(new sylar::Thread(std::bind(&Schedule::run,this),"THREAD_"+std::to_string(i)));
            }
        }
        //如果任务队列中的任务全部被消化了,那么就在这等待每个子线程的结束
        for(size_t i = 0; i < m_poll_size; ++i){
            m_thread_poll[i]->join();
        }

        std::cout << "Schedule stop..." << std::endl; 
    }
    
    //接受调度任务方法
    void schedule(std::function<void()> task){
        std::cout << "Push task into queue " << &task << std::endl;
        //将任务对象依次存放到调度器的任务队列中
        m_tasks_queue.push_back(task);
    }

private:
    //由于是多线程环境下所以提供锁
    MutexType m_mutex;
    //线程池的大小
    size_t m_poll_size = 0;
    //线程池
    std::vector<sylar::Thread::ptr> m_thread_poll;
    //任务队列
    std::list<std::function<void()> > m_tasks_queue;      
};



int main(){
    std::cout << "====Schedule====" << std::endl;
    std::cout << "====Main start====" << std::endl;
    
    {
        //记录当前时间
        auto start = std::chrono::high_resolution_clock::now();
       
        //构建调度器,指定线程池中线程数量
        Schedule sc(2);
        //依次将要执行的任务塞入调度器
        sc.schedule(std::bind(&sing));
        sc.schedule(std::bind(&dance));
        sc.schedule(std::bind(&rap));
        //执行调度器
        sc.start();
        //停止调度器
        sc.stop();
        
        //记录结束时间
        auto end = std::chrono::high_resolution_clock::now();
        //计算花费的时间
        std::chrono::duration<double,std::milli> elapsed = end - start;
        std::cout << "Elapsed: " << (int)elapsed.count() << std::endl;
    }

    std::cout << "====Main end====" << std::endl;

    return 0;
}

以下是输出:

===Schedule===
===Main start====
Push task into queue  0x7ffd602ffb00
Push task into queue  0x7ffd602ffb40
Push task into queue  0x7ffd602ffb80
Schedule start...
Init a thread poll
Schedule run...
THREAD_1 sing~ 100000000
THREAD_2 dance~ 200000000
THREAD_1 rap~ 300000000
Schedule stop...
Elapsed: 234
===Main end====

可以看到以上的代码使用了调度器,代码的编写会很舒服,以下是使用调度器代码的部分:

//构建调度器,指定线程池中线程数量
Schedule sc(2);
//依次将要执行的任务塞入调度器
sc.schedule(std::bind(&sing));
sc.schedule(std::bind(&dance));
sc.schedule(std::bind(&rap));
//执行调度器
sc.start();
//停止调度器
sc.stop();

这样一来,无论是有多少任务需要执行,程序员都不需要关系如何创建线程和如何进行调度了,
只需要把所要执行的任务全部一股脑的交给调度器就行了。
线程也不需要自己来维护了,只需要告诉调度器需要多少个线程就行。
只要调度器完善的够好,程序员就只需要专心处理业务问题就好了。

看到这里相信你对调度器的功能和必要性有了一定的了解,那么我们来继续优化这个调度器,让它更健硕。


二、考虑多线程下的多个调度器问题【线程级别的调度器】

  1. 由于上边代码调度器Schedule类可以在其他任意线程中构建与执行,所以需要考虑多线程下调度器的区分问题。
  2. 由于这里为了由浅入深,所以没有使用协程,那么调度器就必须占用一个线程来执行。

接下来的场景会变成:一个调度器线程和多个其他的线程和一系列的函数任务。
所以需要使用线程局部变量来控制一个线程只存在一个调度器
当然这个判断需要将调度器的this存储起来做比较

以下是控制一个线程只存在一个调度器的核心逻辑

static thread_local Schedule* t_schedule = nullptr;

Schedule(size_t size = 1)
    :m_poll_size(size){
    //断言当前线程是否不存在调度器,如果已存在那么报错
    SYLAR_ASSERT2(GetThis()==nullptr,"one thread one schedule");
    //设置当前线程上的调度器为当前调度器
    setThis();
}

~Schedule(){
	//如果当前调度器就是对应线程上的调度器那么将线程上的调度器清空
    if(GetThis()==this){
        t_schedule = nullptr;
    }
}

static Schedule* GetThis(){
    return t_schedule;
}

void setThis(){
    t_schedule = this;
}

例子(完整代码):

#include <chrono>
#include <iostream>
#include <pthread.h>
#include <vector>
#include <list>
#include <unistd.h>
#include <functional>
#include "sylar/log.h"
#include "sylar/thread.h"
#include "sylar/mutex.h"
#include "sylar/macro.h"

int num = 0;
uint64_t loop_times = 100000000;

void sing(){
    for(size_t i = 0; i < loop_times; ++i){
        ++num;
    }
    std::cout << sylar::Thread::GetName() << " sing~ " << num << std::endl;
}

void dance(){
    for(size_t i = 0; i < loop_times; ++i){
        ++num;
    }
    std::cout <<sylar::Thread::GetName() << " dance~ " << num << std::endl;   
}

void rap(){
    for(size_t i = 0; i < loop_times; ++i){
        ++num;
    }
    std::cout <<sylar::Thread::GetName() << " rap~ " << num << std::endl;   
}

class Schedule;


static thread_local Schedule* t_schedule = nullptr;

class Schedule {
public:
    typedef sylar::Mutex MutexType;

    Schedule(size_t size = 1)
        :m_poll_size(size){
        SYLAR_ASSERT2(GetThis()==nullptr,"one thread one schedule");
        setThis();
        m_callerThread = sylar::Thread::GetThis();
    }
    
    ~Schedule(){
        if(GetThis()==this){
            t_schedule = nullptr;
        }
    }

    static Schedule* GetThis(){
        return t_schedule;
    }

    void setThis(){
        t_schedule = this;
    }
    

    void run(){
        MutexType::Lock lock(m_mutex);
        std::function<void()> task;
        auto it = m_tasks_queue.begin();
        while(it != m_tasks_queue.end()){
            task = *it;
            m_tasks_queue.erase(it);
            break;
        }
        if(task != nullptr){
            task();
        }
    }

    void start(){
        std::cout << "Schedule start..." << std::endl;
        std::cout << "Init a thread poll" << std::endl;
        m_thread_poll.resize(m_poll_size);
        std::cout << "Schedule run..." << std::endl;
        for(size_t i = 0; i < m_poll_size; ++i){
            m_thread_poll[i].reset(new sylar::Thread(std::bind(&Schedule::run,this),"THREAD_"+std::to_string(i)));
        }
    }
    
    void stop(){
        while(!m_tasks_queue.empty()){
            for(size_t i = 0; i < m_poll_size; ++i){
                m_thread_poll[i].reset(new sylar::Thread(std::bind(&Schedule::run,this),"THREAD_"+std::to_string(i)));
            }
        }
        for(size_t i = 0; i < m_poll_size; ++i){
            m_thread_poll[i]->join();
        }


        std::cout << "Schedule stop..." << std::endl; 
    }
    
    void schedule(std::function<void()> task){
        std::cout << "Push task into queue " << &task << std::endl;
        m_tasks_queue.push_back(task);
    }

private:
    MutexType m_mutex;
    size_t m_poll_size = 0;
    std::vector<sylar::Thread::ptr> m_thread_poll;
    std::list<std::function<void()> > m_tasks_queue;      
    sylar::Thread* m_callerThread = 0;
};


//第二个调度线程
void subThread(){
    std::cout << "Sub Schedule Thread" << std::endl;
    Schedule sc(1);
    sc.schedule(std::bind(&rap));
    sc.start();
    sc.stop();
}



int main(){
    std::cout << "====Schedule====" << std::endl;
    std::cout << "====Main start====" << std::endl;
    
    {
        auto start = std::chrono::high_resolution_clock::now();
       
        Schedule sc(2);
        sc.schedule(std::bind(&subThread));
        sc.schedule(std::bind(&dance));
       // sc.schedule(std::bind(&rap));
        sc.start();
        sc.stop();


        auto end = std::chrono::high_resolution_clock::now();
        std::chrono::duration<double,std::milli> elapsed = end - start;
        std::cout << "Elapsed: " << (int)elapsed.count() << std::endl;

    }

    std::cout << "====Main end====" << std::endl;

    return 0;
}

可以看到,调度器可以在任何一个线程中使用,互不影响。


三、加入协程的概念【协程级别的调度器】

一旦加入协程的概念,那么调度器的复杂度就会上升一个层次。
首先要理清楚有哪些东西参与到这个调度器中。
其次要统一称呼,视频中就是很多称呼不明确导致看不懂。
需要理清楚以下几个点:

  1. 加入协程的概念后,调度器就不需要占用一个线程来专门做调度,而是退居到某个线程下的 协程 中去。
  2. 一旦退居到某个协程中,那么该调度器所在的协程我们叫做 【调度协程】
  3. 【调度协程】 所在的线程我称它为 【调度线程】
  4. 在一个线程中会有一个 【主协程】 ,要记住 【调度协程】 可以是 【主协程】 也可能不是(调度线程是主线程时必然不是)。
  5. 在多线程中会有一个 【主线程】,要记住 【调度线程】 可以是 【主线程】 也可以不是。
  6. 调度任务会变成两种类型:【函数任务】、【协程任务】,这里可以把他们包装到一起,统称 【任务】
  7. 【函数任务】最终可以包装成【协程任务】,这样统一调用方式。
  8. 我们封装的协程对象会有很多状态,所以在调度器中需要有比较复杂的判断(这个需要看代码了)。

四、总结

1.要看懂协程调度必须拆分着看,辩证的看。
2.由于协程调度的IO协程篇还没看完,所以存在几个遗留方法,这样无法看到全貌,可以理解大概流程后继续。
3.作者代码中确实有不足的地方,需要先内心赞同他的一切做法,当自己完全看懂后再做修改(比如Fiber中有Schedule的耦合代码)。
4.本人还没完全看完协程调度相关的视频,在看完IO协程调度后可能会回过头来修改这部分的博客内容。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

信息安全管理体系介绍(含全套体系文档)

信息安全管理体系英文全称Information Security Management System&#xff0c;简称为ISMS&#xff0c;是1998年左右从英国发展起来的信息安全领域中的一个全新概念&#xff0c;是管理体系&#xff08;Management System&#xff0c;MS&#xff09;思想和方法在信息安全领域的应…

Flask-大体了解介绍

初识Flask Flask是使用 Python编写的Web微框架。Web框架可以让我们不用关心底层的请求响应处理&#xff0c;更方便高效地编写Web程序。 Flask主要有两个依赖&#xff0c;一个是WSGI&#xff08;Web Server Gateway Interface&#xff0c;Web服务器网关接口&#xff09;工具集…

探索大语言模型在信息提取中的应用与前景

随着人工智能技术的快速发展&#xff0c;大语言模型&#xff08;LLMs&#xff09;在自然语言处理&#xff08;NLP&#xff09;领域取得了显著的进展。特别是在信息提取&#xff08;IE&#xff09;任务中&#xff0c;LLMs展现出了前所未有的潜力和优势。信息提取是从非结构化文本…

ChatGPT-Next-Web漏洞利用分析(CVE-2023-49785)

1. 漏洞介绍 ​ 日常网上冲浪&#xff0c;突然粗看以为是有关Chat-GPT的CVE披露出来了&#xff0c;但是仔细一看原来是ChatGPT-Next-Web的漏洞。漏洞描述大致如下&#xff1a;&#xff08;如果有自己搭建了还没更新的速速修复升级防止被人利用&#xff0c;2.11.3已经出来了&am…

解决ModuleNotFoundError: No module named ‘skfuzzy‘,这个库全名可不叫skfuzzy哦,否则直接报错!!

ModuleNotFoundError: No module named skfuzzy 在这里插入图片描述在这里插入图片描述如何解决 ModuleNotFoundError: No module named skfuzzy 的问题&#xff1f;skfuzzy 模块介绍什么是模糊C均值聚类&#xff1f;skfuzzy 的应用如何使用 skfuzzy 进行模糊聚类 结论 如何解决…

数据结构-线性表-应用题-2.2-14

1&#xff09;算法基本设计思想&#xff1a; 2&#xff09;c语言描述&#xff1a; #define INT_MAX 0X7FFFFFFF int abs_(int a) {//绝对值if(a<0) return -a;else return a; } bool min(int a,int b,int c){if(a<b&&a<c) return true;else return false; } …

JAVA随记——集合篇

注意&#xff1a;作者之前的Java基础没有打牢&#xff0c;有一些知识点没有记住&#xff0c;所以在学习中出现了许多零散的问题。现在特地写一篇笔记总结一下&#xff0c;所以有些知识点不是很齐全。 集合中各类名词的关系 Collection集合为单列集合。 集合存储数据类型的特点…

案例导入说明.md

案例导入说明 为了演示多级缓存&#xff0c;我们先导入一个商品管理的案例&#xff0c;其中包含商品的CRUD功能。我们将来会给查询商品添加多级缓存。 1.安装MySQL 后期做数据同步需要用到 MySQL 的主从功能&#xff0c;所以需要大家在虚拟机中&#xff0c;利用 Docker 来运行一…

即将开幕,邀您共赴创新之旅“2024上海国际消费者科技及创新展览会”

备受期待的2024上海国际消费者科技及创新展览会&#xff08;以下简称“CTIS”&#xff09;即将于6月13日至15日亮相上海新国际博览中心N1-N3馆。 2024上海国际消费者科技及创新展览会总面积达40,000平方米&#xff0c;涵盖600余家展商&#xff0c;预计吸引40,000多位观众莅临现…

js原生写一个小小轮播案例

先上示例&#xff1a; 附上代码 html <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8"><meta http-equiv"X-UA-Compatible" content"IEedge"><meta name"viewport" content…

陪诊系统|陪诊小程序成品|陪诊系统功能

随着人们对健康的日益关注以及医疗技术的不断进步&#xff0c;陪诊小程序应运而生&#xff0c;通过提供陪同就医、医疗服务和健康管理等功能为患者和家庭成员提供了更多的便利和选择。本文将分析陪诊小程序的关键功能&#xff0c;以便更好地理解其在医疗领域的作用。 在陪诊小程…

SpringBoot过滤器简单构建详细教程以及与拦截器区别解释

作用范围&#xff1a;过滤器基于Servlet规范&#xff0c;作用于更广泛的层面&#xff0c;不仅限于Spring MVC&#xff0c;它可以拦截进入Web应用的所有请求&#xff0c;包括静态资源请求。过滤器可以对请求和响应的内容进行预处理和后处理。实现方式&#xff1a;过滤器需要实现…

iPhone 数据恢复软件 – 恢复丢失的 iPhone 数据

恢复丢失的 iPhone 数据&#xff0c;奇客数据恢复iPhone版。如今的 iPhone 用户在他们的设备上存储了大量数据&#xff0c;从照片和与亲人的文本对话到商业和医疗信息。其中一些是保密的&#xff1b;其中大部分内容都是非常个人化的&#xff1b;而且大多数一旦丢失就无法替代。…

4G水电燃气表定时拍照云端识别抄表仪器

通信方式&#xff1a;4G全网通 通信频段&#xff1a;B1/B3/B5/B8/B34/B38/B39/B40/B41 传输速率&#xff1a;最大10Mbps(DL)/最大5Mbps(UL) 传输功率&#xff1a;≤23dBm2dB 图片尺寸&#xff1a;640*480 JPG 图片大小&#xff1a;10~20K 光源条件&#xff1a;自带补光&a…

很好的Baidu Comate,使我的编码效率飞起!

文章目录 背景及简单介绍Baidu Comate安装功能演示总结 &#x1f381;写在前面&#xff1a; 观众老爷们好呀&#xff0c;这里是前端小刘不怕牛牛频道&#xff0c;今天牛牛在论坛发现了一款便捷实用的智能编程助手&#xff0c;就是百度推出的Baidu Comate。下面是Baidu Comate评…

html--互动星空

<!doctype html> <html> <head> <meta charset"utf-8"> <title>互动星空</title><style> html,body {margin:0;overflow:hidden;width:100%;height:100%;cursor:none;background:black;background:linear-gradient(to bot…

CSS-背景属性

目录 背景属性 background-color (背景颜色 ) background-image (背景图片 ) background-repeat (背景图平铺方式 ) no-repeat 不平铺 repeat-x 水平方向平铺 repeat-y 垂直方向平铺 repeat 平铺 background-position (背景图位置) background-size (背景缩…

Apple 添加了 13 英寸 iPad Air

劈啪&#xff01;苹果推出的新款 iPad Air&#xff0c;将所有梦想变为现实&#xff01;它配备了强大的后置 12MP 摄像头和前置 12MP 摄像头&#xff0c;令您的拍摄体验更加出色。苹果还加入了 Apple Pencil 悬停功能&#xff0c;让您的创作更加灵活。 这款 iPad Air 不仅速度加…

antd vue pro (vue 2.x) 多页签详细操作

antd vue pro 多页签配置操作&#xff0c;具体操作如下。 1.引入 tagviews文件 在 store/modules 中创建 tagviews.js &#xff0c;复制一下代码到文件中保存 const state {visitedViews: [],cachedViews: [] }const mutations {ADD_VISITED_VIEW: (state, view) > {if …

相交链表(数据结构)

160. 相交链表 - 力扣&#xff08;LeetCode&#xff09;https://leetcode.cn/problems/intersection-of-two-linked-lists/description/ 题目 解决思路 1&#xff0c;找到相交的点 相交链表的关键也就是找到相交的点&#xff0c;所以我们需要首先判断有没有相交的节点&#…