WebServer重写(一):日志库双缓冲和阻塞队列压测对比

news2024/11/22 6:53:26

目录

          • 前言
          • 重构动机
          • 模块介绍
            • FileUtil,LogFile,LogStream,Logging
            • AsyncLogging(重要)
          • 压测
          • 源码

前言

上次参考TinyWebserver的实现思路是:实现一个blockQueue, 然后实现一个日志接口类,这个接口类承担了打开,写入,关闭日志文件,以及创建一个专门线程用于循环取出队列内的数据。

基于这个思想,上版本实现了这样一个日志库:

#ifndef LOG_H
#define LOG_H

#include <stdio.h>
#include <iostream>
#include <string>
#include <stdarg.h>
#include <pthread.h>
#include "block_queue.h"

using namespace std;

class Log
{
public:
    //C++11以后,使用局部变量懒汉不用加锁
    static Log *get_instance()
    {
        static Log instance;
        return &instance;
    }

    static void *flush_log_thread(void *args)
    {
        Log::get_instance()->async_write_log();
    }
    //可选择的参数有日志文件、日志缓冲区大小、最大行数以及最长日志条队列
    bool init(const char *file_name, int close_log, int log_buf_size = 8192, int split_lines = 5000000, int max_queue_size = 0);

    void write_log(int level, const char *format, ...);

    void flush(void);

private:
    Log();
    virtual ~Log();
    void *async_write_log()
    {
        string single_log;
        //从阻塞队列中取出一个日志string,写入文件
        while (m_log_queue->pop(single_log))
        {
            m_mutex.lock();
            fputs(single_log.c_str(), m_fp);
            m_mutex.unlock();
        }
    }

private:
    char dir_name[128]; //路径名
    char log_name[128]; //log文件名
    int m_split_lines;  //日志最大行数
    int m_log_buf_size; //日志缓冲区大小
    long long m_count;  //日志行数记录
    int m_today;        //因为按天分类,记录当前时间是那一天
    FILE *m_fp;         //打开log的文件指针
    char *m_buf;
    block_queue<string> *m_log_queue; //阻塞队列
    bool m_is_async;                  //是否同步标志位
    locker m_mutex;
    int m_close_log; //关闭日志
};

#define LOG_DEBUG(format, ...) if(0 == m_close_log) {Log::get_instance()->write_log(0, format, ##__VA_ARGS__); Log::get_instance()->flush();}
#define LOG_INFO(format, ...) if(0 == m_close_log) {Log::get_instance()->write_log(1, format, ##__VA_ARGS__); Log::get_instance()->flush();}
#define LOG_WARN(format, ...) if(0 == m_close_log) {Log::get_instance()->write_log(2, format, ##__VA_ARGS__); Log::get_instance()->flush();}
#define LOG_ERROR(format, ...) if(0 == m_close_log) {Log::get_instance()->write_log(3, format, ##__VA_ARGS__); Log::get_instance()->flush();}

输出效果:
2023-01-26 18:16:42.990669 [erro]: MySQL Error
2023-01-26 18:20:25.579885 [erro]: MySQL Error

现在来看,这个日志库存在如下缺点:

1 异步线程直接将阻塞队列的string写入日志,每写一次就flush一次,频繁访问磁盘,而访问磁盘的耗时是极大的;

2 阻塞队列自身存在生产者与消费者的锁竞争,因为生产者和消费者都需要先把队列锁住才能继续对应的操作,如果生产者在正要写队列时消费者锁住了队列,那生产者就只能等待了,这将会影响日志系统整体的吞吐效率。

3 日志采用C stdio格式, 只适合需要格式化输出的数据, 对于那些定义了operator<<的自定义对象并不友好。

4 类设计上,首先太过耦合,Log类包揽文件管理,日志前后缀定义,提供写日志接口,并发互斥等任务,可以拆成几个单元来实现(这也是TinyWebServer所有模块的通病),其次并不需要异步日志开闭操作,实战下肯定都是专门线程异步写日志,哪能让日志阻塞工作线程

重构动机

综上问题,决定重写这个模块。新的模块仿照Muduo, 将包括如下要素:

1 专门的文件工具类以及日志文件类(FileUtil, LogFile)将日志读写从整个过程中解耦;

2 采用双缓冲队列并将日志系统分前后端,前后端各两块缓冲,交替读写,以避免锁竞争,加快效率;

3 采用C++的stream形式,这样就能自动对接定义了operator<<的class, 更灵活;

4 新的日志输出格式,以符合多种需求的需要

image-20230126205315221

这次重构日志的另一个动机是,将其实现为一个更为通用的日志库,这样不仅是WebServer, 以后做的其他程序也能应用进去,这样一旦我以后有哪类信息,比如调试类信息不想和普通信息一起输出到终端,我就能通过日志把它们输出到文件中去,实现消息分流。

模块介绍
FileUtil,LogFile,LogStream,Logging

模块主要分为5个部分,FileUtil,LogFile,LogStream,Logging 和 AsyncLogging, 其中前三个类是基础,第四个和第五个分别是接口和实现。

FileUtil实现了一个scope生命期的File class,一次append写入一行; LogFile作为日志专用类,在FileUtil基础上加入了一个每写入几行就将内容flush入硬盘的机制,减少访问磁盘的次数,LogStream是自己实现了一个stream类,Logging作为接口类,通过构造和构析的方式分别为LogStream缓冲中放入的内容添加前后缀(也就是前面的日期 类型 线程号 以及后面的文件名及具体行)并最终输出(还挺有意思,不知道为什么不用单例,根据陈硕的说法是压测效率并不低),这些都比较简单,不详细叙述。

AsyncLogging(重要)

AsyncLogging是实现的核心,之前说的双缓冲队列实现高效写的思路也就在这里,实际上这个类初始共配置了4个缓冲,前端和后端各两块,前端为currBuf_, nextBuf_, 后端为newBuffer1和newBuffer2。为了避免无谓拷贝和不必要的引用,这些Buffer都以堆的形式创建并使用unique_ptr管理。

#include"LogFile.h"
#include"LogStream.h"
#include"LoggingImpl.h"
#include<vector>
#include<memory>
#include<mutex>
#include<condition_variable>
#include<semaphore.h>
#include<thread>
class AsyncLogging : public LoggingImpl{
    public:
        typedef FixedBuffer<kLargeBuffer> Buffer;   //4mb = 1000 logs
        typedef std::unique_ptr<Buffer> BufferPtr; 
        typedef std::vector<BufferPtr> BufferVector;
        typedef std::mutex MutexLock;
        typedef std::unique_lock<std::mutex> MutexLockGuard;
        typedef std::condition_variable Condition;
        AsyncLogging(const char* filename, int flushInterval = 2);
        ~AsyncLogging();
        void start();
        void append(const char* msg, int len);
        void stop(){
            running_ = false;
            cond_.notify_all();
            thread_->join();
        }
    private:
        void threadFunc();
    private:
        LogFile file_;
        int flushInterval_;
        bool running_;
        BufferPtr currBuf_;
        BufferPtr nextBuf_;
        BufferVector readyBufs_;    //queue
        MutexLock mtx_;
        Condition cond_;
        std::unique_ptr<std::thread> thread_;
};

写入的日志首先被写入前端,填充的是currBuf_currBuf_一旦被填满之后就会被加入预备队列readyBufs_并通过条件变量唤醒写线程,同时将nextBuf_移动赋给currBuf_完成生产缓冲的“接棒”。

void AsyncLogging::append(const char* msg, int len){
    assert(running_);
    MutexLockGuard guard(mtx_);
    if(currBuf_->avail() > len){
        currBuf_->append(msg, len);
    }
    else{
        readyBufs_.push_back(std::move(currBuf_));
        //reload currBuf_
        if(nextBuf_){
            currBuf_ = std::move(nextBuf_);
            //nextBuf_ will be reload by thread;
        }
        else{
            currBuf_ .reset(new Buffer());
        }
        currBuf_->append(msg,len);
        cond_.notify_all();
    }

这里有一个特殊情况,就是可能由于生产者生产太快,前端的两个Buffer已经都拿到后端去写了,前端实际无Buffer可用,这时候前端只能给自己“扩容”,new一个Buffer出来作为备用。

后端线程的处理是最复杂的部分,首先线程会给自己初始化newBuffer1和newBuffer2, 这两个buffer将会用于currBuf_nextBuf_的更新,并准备一块写队列bufferForWrite, 线程被调用时将会尝试将待写队列readyBufs_ swap到bufferForWrite开始将缓存内容逐个写入到底层LogFile的buffer中。注意这里对readyBufs_的等待条件是waitForSeconds, 目的是实现日志的间隔刷新,以避免日志写入频率极慢的情况下currBuf_写不完无法输出的情况。因此currBuf_被加入readyBufs_的情况有两个,一是被写满,二是写不完超时。

void AsyncLogging::threadFunc(){
    pthread_setname_np(pthread_self(),"Logger");
    BufferPtr newBuffer1(new Buffer);
    BufferPtr newBuffer2(new Buffer);
    newBuffer1->bzero();
    newBuffer2->bzero();
    BufferVector bufferForWrite;
    bufferForWrite.reserve(16);

    while(running_){
        assert(newBuffer1 && newBuffer1->length() == 0);
        assert(newBuffer2 && newBuffer2->length() == 0);
        {
            MutexLockGuard guard(mtx_);
            if(readyBufs_.empty()){
                cond_.wait_for(guard, sec(flushInterval_));
            }
            readyBufs_.push_back(std::move(currBuf_));
            currBuf_ = std::move(newBuffer1);
            bufferForWrite.swap(readyBufs_);
            if(!nextBuf_)
                nextBuf_ = std::move(newBuffer2);
        }
        
        assert(!bufferForWrite.empty());

        //设置buffer超过25即丢弃
        if(bufferForWrite.size() > 25){
            char buf[256] = {0};
            snprintf(buf,sizeof(buf),"too much buffers, dropped %ld buffers\n", bufferForWrite.size() - 2);
            file_.append(buf, strlen(buf));
            bufferForWrite.erase(bufferForWrite.begin() + 2, bufferForWrite.end());
        }

        //允许输出[3,25]个缓冲
        for(auto &buf : bufferForWrite){
            file_.append(buf->data(),buf->length());
        }

        //最终确保后台buffer数保持2个
        if(bufferForWrite.size() > 2){
            bufferForWrite.resize(2);
        }


        //overload newbuffer1, newbuffer2;
        if(!newBuffer1){
            assert(!bufferForWrite.empty());
            newBuffer1 = std::move(bufferForWrite.back());
            bufferForWrite.pop_back();
            newBuffer1->reset();
        }

        if(!newBuffer2){
            assert(!bufferForWrite.empty());
            newBuffer2 = std::move(bufferForWrite.back());
            bufferForWrite.pop_back();
            newBuffer2->reset();
        }
     
        file_.flush();
        bufferForWrite.clear();
    }
    file_.flush();
}

如果日志的生产速度极快,超过了日志系统后端的处理能力(如文件读写速度)就会造成日志堆积,表现为前端不断new Buffer给currBuf_来应对频繁的日志请求,如果后端没法及时的处理在buffersToWrite中的Buffer的话,readyBufs_中的待写缓冲将会不断堆积,最终导致程序内存不足发生崩溃。

muduo日志系统对该情况的处理方法是直接丢弃堆积的日志,强制将buffersToWrite内的buffer缩减至2个,虽然做法简单粗暴,但一来保证程序的稳定运行,二来可以扩展一个报警程序,简化问题定位,为后续的优化做准备。

buffersToWrite的缓冲都写完之后,内部缓冲将会栈的顺序重新赋给newBuffer1和newBuffer2,为下次的currentBuffer和nextBuffer留好预备。

总结下来,四个缓冲的循环被写的顺序是:currBuf_, nextBuf_,newBuffer1, newBuffer2

----------------------------------------currBuf_  <----move------ nextBuf_
\                                            ^                       ^
 \					                         |                       |          
 |										 newBuffer1	            newBuffer2
  \                                            ^                    ^
   \										   |	                |       
  	\-- swap--> bufferForWrite --> pop_back----|--------------------| 

自此整个日志系统的架构就完成了,我还写了个阻塞队列的版本来和双缓冲比较,下面只展示阻塞队列版实现代码,具体结果见压测部分。

BlockQueue.h

#pragma once
#include<deque>
#include<limits.h>
#include<cassert>
#include<unistd.h>
#include<mutex>
#include<condition_variable>
#include<thread>
typedef std::mutex MutexLock;
typedef std::unique_lock<std::mutex> MutexLockGuard;
typedef std::condition_variable Condition;

//BlockQueue make sure only one thread can access this queue per time
template <typename T>
class BlockQueue{
    public:
        BlockQueue(size_t maxSize = 5000) : maxSize_(maxSize), isClose_(false){};
        ~BlockQueue(){ close(); };
        void push(const T& obj);
        bool pop();
        bool pop(T& item);
        T front();
        T back();
        bool empty();
        bool size();
        void close();

    private:
        size_t maxSize_;
        bool isClose_;
        MutexLock mtx_;
        Condition condProducer_;
        Condition condConsumer_;
        std::deque<T> deq_;
        
};

template <typename T>
void BlockQueue<T>::push(const T& obj){
    MutexLockGuard guard(mtx_);
    while(deq_.size() >= maxSize_){
        condProducer_.wait(guard);
    }
    deq_.push_back(obj);
    condConsumer_.notify_one();
};

template <typename T>
bool BlockQueue<T>::pop(){
    MutexLockGuard guard(mtx_);
    while(deq_.empty() && !isClose_){
        condConsumer_.wait(guard);
    }
    if(isClose_)
        return false;
    assert(!deq_.empty());
    deq_.pop_front();
    condProducer_.notify_one();
    return true;
};

template <typename T>
bool BlockQueue<T>::pop(T& item){
    MutexLockGuard guard(mtx_);
    while(deq_.empty() && !isClose_){
        condConsumer_.wait(guard);
    }
    if(isClose_)
        return false;
    assert(!deq_.empty());
    item = deq_.front();
    deq_.pop_front();
    condProducer_.notify_one();
    return true;
};

template <typename T>
T BlockQueue<T>::front(){ 
    MutexLockGuard guard(mtx_);
    assert(!deq_.empty());
    return deq_.front(); 
}

template <typename T>
T BlockQueue<T>::back(){
    MutexLockGuard guard(mtx_);
    assert(!deq_.empty());
    return deq_.back(); 
}

template <typename T>
bool BlockQueue<T>::empty(){
    MutexLockGuard guard(mtx_);
    return deq_.empty();
}

template <typename T>
bool BlockQueue<T>::size(){
    MutexLockGuard guard(mtx_);
    return deq_.size();
}

template <typename T>
void BlockQueue<T>::close(){
    {
        MutexLockGuard guard(mtx_);
        deq_.clear();
        isClose_ = true;
    }
    condProducer_.notify_all();
    condConsumer_.notify_all(); 
};
//除了currBuf_, nextBuf_, readyBufs_这些被换成BlockQueue外其他均不变
#include"AsyncLoggingBlockQueue.h"
#include<cassert>
#include<chrono>
#include<functional>
typedef std::chrono::seconds sec;
using std::string;
AsyncLoggingBlockQueue::AsyncLoggingBlockQueue(const char* filename, int flushInterval)
:file_(filename), flushInterval_(flushInterval), running_(false){
    sem_init(&latch_,0,0);
}

void AsyncLoggingBlockQueue::start(){
    assert(!running_);
    running_ = true;
    thread_.reset(new std::thread(std::bind(&AsyncLoggingBlockQueue::threadFunc, this)));
    sem_post(&latch_);
}

void AsyncLoggingBlockQueue::append(const char* msg, int len){
    MutexLockGuard guard(mtx_);
    queue_.push(string(msg,len));
}

void AsyncLoggingBlockQueue::threadFunc(){
    pthread_setname_np(pthread_self(),"Logger");
    sem_wait(&latch_);  //确保生产者先于消费者
    string log;
    while(queue_.pop(log)){
        MutexLockGuard guard(mtx_);   
        file_.append(log.c_str(), log.size());
    }
}

AsyncLoggingBlockQueue::~AsyncLoggingBlockQueue(){
    if(running_) stop();
}
压测

压测方法:分长日志字符和短日志字符,共30轮,每轮输出1000次,共30000次,计算输出这30000次日志的时间(单位为毫秒)。

压测目的:验证和比较双缓冲和阻塞队列两种实现下的写日志效率

双缓冲版本代码:

#include"../base/Logging.h"
#include<iostream>
#include<time.h>
#include<unistd.h>
#include<string>
using std::string;


off_t kRollSize = 500*1000*1000;
void myLogBench(bool longLog, int round, int kBatch){
    Logger::setLogFileName("../../logFile/myLogAsync");
    int cnt = 0;
    string empty = " ";
    string longStr(3000,'X');

    clock_t start = clock();
    for(int t = 0; t < round; ++t){
        for(int i = 0; i < kBatch; ++i){
            LOG_INFO << "Hello 123456789" << " abcdefghijklmnopqrstuvwxyz " << (longLog ? longStr : empty) << cnt;
            ++cnt;
        }
    }
    clock_t end = clock();
    printf("%lf\n", (float)(end-start)*1000 / CLOCKS_PER_SEC);
}


int main(){
    printf("program started, pid = %d\n", getpid()),
    myLogBench(false, 30, 1000);
    myLogBench(true, 30, 1000);

}

阻塞队列版本代码,为了能够同时调试两个版本的代码,特意对Logger类做了扩展,使其能够设置具体的日志实现。

#include"../base/Logging.h"
#include"../base/LoggingImpl.h"
#include"../base/AsyncLoggingBlockQueue.h"
#include<iostream>
#include<time.h>
#include<unistd.h>
#include<string>
using std::string;


off_t kRollSize = 500*1000*1000;
void myLogBench(bool longLog, int round, int kBatch){
	Logger::setLogFileName("../../logFile/myLogAsyncBlockQueue");
    Logger::setLogImpl(new AsyncLoggingBlockQueue(Logger::getLogFileName().c_str()));
    int cnt = 0;
    string empty = " ";
    string longStr(3000,'X');

    clock_t start = clock();
    for(int t = 0; t < round; ++t){
        for(int i = 0; i < kBatch; ++i){
            LOG_INFO << "Hello 123456789" << " abcdefghijklmnopqrstuvwxyz " << (longLog ? longStr : empty) << cnt;
            ++cnt;
        }
    }
    clock_t end = clock();
    printf("%lf\n", (float)(end-start)*1000 / CLOCKS_PER_SEC);
}


int main(){
    printf("program started, pid = %d\n", getpid());
    myLogBench(false, 30, 1000);
    myLogBench(true, 30, 1000);

}

测试和比较

阻塞队列和我实现的四个缓冲对比

root@iZbp13zqzr3c74v3o1ry3mZ:/home/LinuxC++/Project/re_webserver/test/build# ./AsyncLoggingBlockQueue_test 
program started, pid = 1130994
584.836975
791.396973
root@iZbp13zqzr3c74v3o1ry3mZ:/home/LinuxC++/Project/re_webserver/test/build# ./AsyncLogging_test 
program started, pid = 1131046
123.179001
292.510010

可以看到同样输出3万条日志,短日志情况下,双缓冲要比阻塞队列快5倍,长日志情况下双缓冲也比阻塞队列快约2.6倍,3万条短日志花了约123毫秒,长日志为292毫秒,也就是每秒双缓冲能写30万条段日志和9万条长日志,而阻塞队列只能写6万条短日志和2万条长日志,综上,吞吐率显著提升。

不过相比原版muduo的每秒100-200万日志,我的山寨版本显然性能很低,和Linyacool的实现在同一个数量级(不同的是linyacool用的是Linux系统pthread库自带的锁,我则全部换成了C++11版本),这说明我还有很多可以优化的细节,但这就超出我做这个server的目的了。

我的实现
107.847000
270.713989

linyacool
85.678001
242.023010

muduo
20.646000
151.054001
源码

https://gitee.com/tracker647/my-muduo-log

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

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

相关文章

ROS小车研究笔记1/31/2023 小车硬件结构及键盘移动控制节点

1 小车硬件结构 1 中控设备 上方的单片机用于控制电机运动&#xff0c;搭载wifi模块和电量显示屏。下方为树莓派&#xff0c;安装了ROS系统和Ubuntu系统&#xff0c;用于整个小车控制。显示屏和树莓派相连 2 传感器系统 激光雷达及转换器。激光雷达和转换器相连&#xff0…

【Rust】7. 枚举和模式匹配

7.1 枚举&#xff08;可存储不同类型的值&#xff09; 7.1.1 基本概念 7.1.2 枚举的简洁用法&#xff1a;构造函数 7.1.3 枚举的优势&#xff1a;处理不同类型和数量的数据 枚举成员的类型&#xff1a;字符串、数字类型、结构体、枚举注意&#xff1a;在未将标准库枚举引入当…

Java——两两交换链表中的节点

题目链接 leetcode在线oj题——两两交换链表中的节点 题目描述 给你一个链表&#xff0c;两两交换其中相邻的节点&#xff0c;并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题&#xff08;即&#xff0c;只能进行节点交换&#xff09;。 题目示例 …

Vue中的$children与$parent讲解

$children与$parent直接演示代码父组件&#xff1a;<template><div><h2>BABA有存款: {{ money }}</h2><button>找小明借钱100</button><br /><button>找小红借钱150</button><br /><button>找所有孩子借钱2…

Day875.怎么给字符串字段加索引 -MySQL实战

怎么给字符串字段加索引 Hi&#xff0c;我是阿昌&#xff0c;今天学习记录的是关于怎么给字符串字段加索引的内容。 现在&#xff0c;几乎所有的系统都支持邮箱登录&#xff0c;如何在邮箱这样的字段上建立合理的索引。 假设&#xff0c;现在维护一个支持邮箱登录的系统&…

【计算机图形学(译)】 一、介绍

【计算机图形学(译&#xff09;】 一、介绍1 介绍 Introduction1.1 图形领域 (Graphics Areas)1.2 主要应用 (Major Applications)1.3 图形APls (Graphics APIs)1.4 图形管线 (Graphics Pinpline)1.5 数值问题 (Numerical Issues)1.6 效率 (Efficiency)1.7 设计和编写图形程序 …

Detectron2部署教程,着重ONNX(从官网翻译)

本教程翻译至这里 https://detectron2.readthedocs.io/en/latest/tutorials/deployment.html detectron2模型训练以后如果想要部署&#xff0c;就需要导出专门的模型才可以。 三种模型导出方式 detectron2支持的模型导出方式有&#xff1a; tracing 该方式导出的格式是Torch…

常量池/String常见面试题

目录 常量池与运行时常量池 字符串常量池String_Table 字符串变量拼接 字符串常量拼接 字符串延迟加载 字符串intern方法 总结StringTable的特点 常量池与运行时常量池 二进制字节码包括 类的基本信息,常量池,类方法定义(包含虚拟机指令) class文件中除了有类的版本,字…

新突破:科学家发现全新的量子纠缠效应

布鲁克海文国家实验室&#xff08;图片来源&#xff1a;网络&#xff09;布鲁克海文国家实验室的科学家发现了一种全新的量子纠缠效应&#xff0c;即使宇宙距离相隔广阔&#xff0c;量子纠缠也会使粒子奇迹般地联系在一起。这一发现使他们能够捕捉到原子内部的奇特世界。这项研…

【算法自由之路】二叉树的递归套路

【算法自由之路】二叉树的递归套路 预热&#xff0c;二叉树的后继节点 话不多说&#xff0c;首先是一道练手题&#xff0c;寻找二叉树任意给定节点的后继节点&#xff0c;此二叉树具备一个指向父节点的指针。 后继节点&#xff1a;在中序遍历中于给定节点后一个打印的节点 p…

SpringBoot实现配置文件的加密和解密

一、项目搭建 1.新建一个springBoot项目 pom.xml <?xml version"1.0" encoding"UTF-8"?> <project xmlns"http://maven.apache.org/POM/4.0.0" xmlns:xsi"http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocatio…

DAMO-YOLO : A Report on Real-Time Object Detection Design

DAMO-YOLO:实时目标检测设计报告在本报告中&#xff0c;我们提出了一种被称为DAMO-YOLO的快速准确的物体检测方法&#xff0c;该方法比最先进的YOLO系列具有更高的性能。DAMO-YOLO是由YOLO扩展而来的&#xff0c;它采用了一些新技术&#xff0c;包括神经结构搜索(NAS)、高效的重…

LeetCode——1669. 合并两个链表

一、题目 给你两个链表 list1 和 list2 &#xff0c;它们包含的元素分别为 n 个和 m 个。 请你将 list1 中下标从 a 到 b 的全部节点都删除&#xff0c;并将list2 接在被删除节点的位置。 下图中蓝色边和节点展示了操作后的结果&#xff1a; 请你返回结果链表的头指针。 来…

【Qt】3.菜单栏、工具栏、状态栏、铆接部件、核心部件、资源文件

目录 菜单栏 工具栏 代码 mainwindow.cpp 结果 状态栏 铆接部件 核心部件 代码 mainwindow.cpp 结果 资源文件 代码 mainwindow.cpp 结果 菜单栏 只能有一个 menuBar()返回一个QMenuBar *bar 将bar放入到窗口中setMenuBar(bar) 添加菜单addMenu("文件&…

三年了,回村了

今年回老家了&#xff0c;因为工作和疫情等原因已经三年多没回了&#xff0c;思乡之情已经压不住了。 老家是一个五线小城市&#xff0c;属于南方典型的鱼米之乡&#xff1a;依山傍水、山清水秀。同时还有一个知名白酒厂&#xff1a;白云边&#xff0c;经济发展还不错。 老家…

从“语义网”到“去中心化”,Web3.0到底是个啥?

什么是Web3.0&#xff0c;为什么近两年这个概念又再一次火出了圈&#xff0c;但凡A股上市公司正在做或者准备做的业务与它沾上边&#xff0c;总会有那么几次异动。 这个概念到底是金融市场布下的骗局&#xff0c;还是未来互联网发展的趋势&#xff0c;在大家的眼里都是褒贬不一…

Redis 核心原理串讲(下),架构演进之高扩展

文章目录Redis 核心原理总览&#xff08;全局篇&#xff09;前言一、数据分片1、集群&#xff1f;2、分片&#xff1f;3、分片固定&#xff1f;4、元数据二、集群1、代理集群2、分片集群3、代理 分片集群三、生产实践总结Redis 核心原理总览&#xff08;全局篇&#xff09; 正…

新的一年,如何打开超级APP发展格局

本文开始我们先来明确一个概念&#xff1a;超级APP是什么&#xff1f;百度百科的定义是——那些拥有庞大的用户数&#xff0c;成为用户手机上的"装机必备”的基础应用。实际上各大互联网平台也给出了不同的解释&#xff0c;但相同点是他们都认为超级APP就应该超级个性化&a…

简单手写后门Safedog检测绕过

今天继续给大家介绍渗透测试相关知识&#xff0c;本文主要内容是简单手写后门Safedog检测绕过。 免责声明&#xff1a; 本文所介绍的内容仅做学习交流使用&#xff0c;严禁利用文中技术进行非法行为&#xff0c;否则造成一切严重后果自负&#xff01; 再次强调&#xff1a;严禁…

最长上升子序列、最长公共子序列、最长公共上升子序列(LIS、LCS、LCIS)

LIS、LCS、LCIS最长上升子序列LIS最长公共子序列LCS最长公共上升子序列LCIS最长上升子序列LIS 题目链接&#xff1a;AcWing895. 最长上升子序列 这里只说明O(n2)O(n^2)O(n2)的解法&#xff0c;O(nlogn)O(nlogn)O(nlogn)解法之前的博客有介绍 O(n2)O(n^2)O(n2)的解法较为容易理…