Linux网络之多路转接——实用的epoll

news2024/9/23 15:20:04

目录

一、高级IO

1.1 概念

1.2 五种IO模型

1.3 小结

二、多路转接的实用派

2.1 epoll 接口

2.1.1 epoll_create 

2.1.2 epoll_ctl 

2.1.3 epoll_wait 

2.2 epoll 底层原理

2.2.1 epoll_ctl

2.2.2 epoll_wait

2.2.3 epoll_create

三、 epoll 类的编写 

3.1 类的框架

3.1.1 私有成员 

3.1.2 构造函数 

3.1.3 析构函数 

3.2  类的执行 Loop

3.2.1 Loop 框架

3.2.2 handlerEvent

四、epoll 的优点


一、高级IO

1.1 概念

了解了网络通信相关的知识后,我们也许能直到,通信的本质就是IO,通信的核心是在两个或多个设备之间传输数据,这与计算机系统中的输入输出操作类似。
当通信时使用接收端口例如 recv 时,系统等待网络数据包从远程服务器通过网络传输到本地机器,数据包从网卡的硬件缓冲区复制到系统内存中的应用程序缓冲区;当文件读取时,系统等待磁盘将所请求的数据读取到磁盘缓冲区中,数据从磁盘缓冲区复制到系统内存中的用户空间。
所以换种说法,IO = 等待 + 拷贝

那么如何提高IO的效率呢?
当缩小了等待的时间后,IO的效率就会提高。 

1.2 五种IO模型

然后,从一个钓鱼的实例引入今天的主题:

将上面的例子抽象成通信IO:


水池:OS内部缓冲区
水桶:用户缓冲区
鱼:数据
鱼竿:文件描述符

上面的五个人物分别对应了五种IO模型:


其中,前四个人都属于同步IO,即只要参与了IO的过程,那就是同步IO。田七将钓鱼的工作交给了另一个人,并没有参与IO,所以是异步IO。

阻塞 IO:在内核将数据准备好之前,系统调用会一直等待。所有的套接字,默认都是阻塞方式。阻塞 IO 是最常见的 IO 模型。

非阻塞 IO:如果内核还未将数据准备好,系统调用仍然会直接返回,并且返回 EWOULDBLOCK 错误码。非阻塞 IO 往往需要程序员循环的方式反复尝试读写文件描述符,这个过程称为轮询。这对 CPU 来说是较大的浪费,一般只有特定场景下才使用。

信号驱动 IO:内核将数据准备好的时候,使用 SIGIO 信号通知应用程序进行 IO 操作。

IO 多路转接: 虽然从流程图上看起来和阻塞 IO 类似。实际上最核心在于 IO 多路转接能够同时等待多个文件描述符的就绪状态。

异步 IO: 由内核在数据拷贝完成时,通知应用程序(而信号驱动是告诉应用程序何时可以开始拷贝数据)。

1.3 小结

任何 IO 过程中,都包含两个步骤。第一是等待,第二是拷贝。而且在实际的应用场景中,等待消耗的时间往往都远远高于拷贝的时间。让 IO 更高效,最核心的办法就是让等待的时间尽量少。

二、多路转接的实用派

上面介绍的五人中,只有赵六真正实现了减少等待的时间,所以在IO中可以使用多路转接以达到高校IO,这里我们要介绍的就是多路转接中的实用派 —— epoll 。它相比之前说的 select ,改进了不少,是目前很多厂商使用多路转接都会用的方法。被公认为 Linux2.6 下性能最好的多路IO就绪通知方法。

2.1 epoll 接口

这里先简单认识一下 epoll 的接口, 2.2 会深入将有关 epoll 的底层逻辑,可以直接跳转到 2.2 来了解 epoll 的底层,届时会有图解,配合图解来理解 epooll 接口。

2.1.1 epoll_create 

int epoll_create(int size);

创建一个 epoll 的句柄
• 自从 linux2.6.8 之后, size 参数是被忽略的.
• 用完之后, 必须调用 close()关闭.

2.1.2 epoll_ctl 

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

 epoll 的事件注册函数

• 它不同于 select()是在监听事件时告诉内核要监听什么类型的事件,而是在这里先注册要监听的事件类型
• 第一个参数是 epoll_create()的返回值(epoll 的句柄)
• 第二个参数表示动作, 用三个宏来表示
• 第三个参数是需要监听的 fd
• 第四个参数是告诉内核需要监听什么事

struct epoll_event 结构如下,这里简单认识一下,后面会具体来讲:

events 可以是以下几个宏的集合:
• EPOLLIN : 表示对应的文件描述符可以读 (包括对端 SOCKET 正常关闭);
• EPOLLOUT : 表示对应的文件描述符可以写;
• EPOLLPRI : 表示对应的文件描述符有紧急的数据可读 (这里应该表示有带外数据到来);
• EPOLLERR : 表示对应的文件描述符发生错误;
• EPOLLHUP : 表示对应的文件描述符被挂断;
• EPOLLET : 将 EPOLL 设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的(这两种触发后面会讲);
• EPOLLONESHOT: 只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个 socket 的话,需要再次把这个 socket 加入到 EPOLL 队列里。

2.1.3 epoll_wait 

int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);

收集在 epoll 监控的事件中已经发送的事件
• 参数 events 是分配好的 epoll_event 结构体数组。
• epoll 将会把发生的事件赋值到 events 数组中 (events 不可以是空指针,内核只负责把数据复制到这个 events 数组中,不会去帮助我们在用户态中分配内存)。
• maxevents 告知内核这个 events 有多大, 这个 maxevents 的值不能大于创建
epoll_create() 时的 size.
• 参数 timeout 是超时时间 (毫秒, 0 会立即返回, -1 是永久阻塞).
• 如果函数调用成功, 返回对应 I/O 上已准备好的文件描述符数目, 如返回 0 表示已超时, 返回小于 0 表示函数失败.

2.2 epoll 底层原理

2.2.1 epoll_ctl

在创建、使用epoll时,OS会先为我们在内部创建一棵红黑树,这颗红黑树以文件描述符作为key值,每个节点包含的信息大致如下图所示,红黑树的含义是内核要关心哪些 fd 的哪些事件,也就是节点中的前两个信息:

short events 即:

与红黑树相关的系统调用为 epoll_create :

epfd:暂时还不关心。
op:当用户使用 ADD 时,epoll_ctl 使用用户传入的文件描述符与事件创建新节点,并将其插入到红黑树中;当用户使用DEL时, epoll_event 不需要传入参数,仅需要传入 fd ,系统就会将该节点从红黑树中删除。
*event:表示用户需要系统帮助关心文件描述符的事件,如 EPOLLIN、EPOKKOUT 分别表示关心读事件与写事件。

2.2.2 epoll_wait

操作系统内部除了一棵红黑树,还会维护一个双向链表形式的就绪队列

其中,当红黑树中有 fd 就绪时,就会将其添加到就绪队列中:

这时,就可以使用 epoll_wait ,它可以将就绪队列中的 fd 添加到用户传入的数组中:

此时,作为应用层,检测有没有事件就绪的时间复杂度为O(1),相比于 select 确实进步了很多。
同时,因为就绪队列中已就绪的 fd 严格按照数组下标放入数组,所以以后在遍历的时候,也只需要遍历 epoll_wait 的返回值个数,而不需要遍历整个传入的数组,这点相较于 select 也是进步

上面说了操作系统会自主把已就绪的 fd 添加到就绪队列中,这里其实设置了一个回调函数,当有 fd 就绪时,就会调用该回调函数,完成回调方法,所以 epoll 模型就是由这三部分组成(两种数据结构 + 一种函数调用):

2.2.3 epoll_create

Linux 一切皆文件,它将 epoll 模型也归结成了文件,在调用 epoll_create 时,操作系统会在文件描述符表中创建一个文件描述符,它指向底层的 epoll 模型!

这也就是为什么 epoll_create 返回一个文件描述符,而 epoll_ctl 与 epoll_wait 都需要传入一个文件描述符。它们都需要该文件描述符才能找到底层的 epoll 模型。此外, task_struct 虽然创建了 epoll 模型,而 epoll 管理的就是它表中的文件描述符。

三、 epoll 类的编写 

3.1 类的框架

对于 epoll_server ,我们还是从 epoll_echo 入手,理解 epoll 模型的调用与使用。同时,在 epoll_server 中,有用到相关的头文件,下面不再重复,可以去 select_server 中找到,链接如下:Linux网络之多路转接——老派的select-CSDN博客

3.1.1 私有成员 

epoll 与 select 相似,但它们都是对报文进行多路转接,所以与之前编写的 TCP 协议一样,都是需要端口号与 listen 套接字。除此之外,通过之前对 epoll_create 的讲解, OS 底层创建的 epoll 模型其实也是文件,所以需要设置一个文件描述符标识 epoll 模型,以便于后面使用 epoll 模型时,OS 可以根据该 fd 找到 epoll 模型。最后,在 epoll_wait 中也介绍到, OS 帮助我们将就绪队列中的文件描述符递交给应用层,这里就需要我们定义一个结构体数组,为 epoll_wait 提供输入参数。

#include <sys/epoll.h>
#include <memory>
#include "Log.hpp"
#include "Socket.hpp"
using namespace socket_ns;

class EpollServer
{
    static const int gnum = 64;

public:
private:
    uint16_t _port;
    std::unique_ptr<Socket> _listensock;
    int _epfd;

    struct epoll_event _revs[gnum];
};

3.1.2 构造函数 

在构造函数中,首先是对端口号的初始化;其次,需要对 listen 套接字进行创建与初始化;然后,需要创建 epoll 模型,并对 epfd 进行初始化;最后,将监听套接字添加到 epoll 实例,并设置关心读事件。

对于最后将 listensock 添加到 epoll 中的解释:监听套接字的主要作用是接受新的客户端连接。当一个新的客户端尝试连接服务器时,监听套接字会变为可读状态(即有新连接到达),可以确保在有新的连接到达时,epoll 会通知程序,触发相应的处理逻辑(如调用 accept 接受新连接)。

除此之外,因为创建 epoll 模型时可能会出错,所以这里将 Socket.hpp 中的错误原因新增了一条 

enum
{
    SOCKET_ERROR = 1,
    BIND_ERROR,
    LISTEN_ERROR,
    USAGE_ERROR,
    EPCREATE_ERROR
};
class EpollServer
{
    static const int gnum = 64;

public:
    EpollServer(uint16_t port) : _port(port), _listensock(std::make_unique<TcpSocket>()), _epfd(-1)
    {
        // 1. 创建listensock
        InetAddr addr("0", _port);
        _listensock->BuildListenSocket(addr);

        // 2. 创建epoll模型
        _epfd = ::epoll_create(128);
        if (_epfd < 0)
        {
            LOG(FATAL, "epoll_create error\n");
            exit(EPCREATE_ERROR);
        }
        LOG(DEBUG, "epoll_create success, epfd: %d\n", _epfd);
        // 3. 将监听套接字添加到 epoll 实例
        struct epoll_event ev;
        ev.events = EPOLLIN;
        ev.data.fd = _listensock->SockFd(); 
        epoll_ctl(_epfd, EPOLL_CTL_ADD, _listensock->SockFd(), &ev);
    }

private:
    uint16_t _port;
    std::unique_ptr<Socket> _listensock;
    int _epfd;

    struct epoll_event _revs[gnum];
};

3.1.3 析构函数 

在用完 epoll 模型后,必须将指向 epoll 模型的文件描述符关闭,同时将监听套接字也关闭。

    ~EpollServer()
    {
        _listensock->Close();
        if (_epfd >= 0)
            ::close(_epfd);
    }

3.2  类的执行 Loop

3.2.1 Loop 框架

上面介绍 epoll_wait 时,已经介绍过,使用 epll_wait 后,我们直接传入结构体数组就可以得到底层中已就绪的套接字,相比于 select 反复的遍历,不断的更新,可以说简化了非常多:

可以看到, epoll 调用一下 epoll_wait 就可以完成 select 中对于标识 fd 数组的类成员 _fd_array 的反复遍历。随后再根据 epoll_wait 的返回值(添加到传参数组的 fd 个数)来确认是否执行成功。

因为 epoll_wait 的返回值比较特殊,它返回的是添加到传参数组的 fd 个数,而且就绪队列会将 fd 严格按照数组下标大小添加到数组中,所以在使用具体的处理函数时,只需要遍历其返回值个数次即可。所以这里向处理函数传入其返回值,便于处理函数遍历数组。 

    void Loop()
    {
        int timeout = -1;
        while (true)
        {
            int n = ::epoll_wait(_epfd, _revs, gnum, timeout);
            switch (n)
            {
            case 0:
                LOG(DEBUG, "epoll_wait timeout...\n");
                break;
            case -1:
                LOG(DEBUG, "epoll_wait failed...\n");
                break;
            default:
                LOG(DEBUG, "epoll_wait haved event ready..., n : %d\n", n);
                handlerEvent(n);
                break;
            }
        }
    }

3.2.2 handlerEvent

handlerEvent 主要有三层:
第一层:遍历类成员中 struct epoll_event _revs[gnum] 的前 n (epoll_wait的返回值) 个元素
第二层:数组元素中的事件 & 关心事件(EPOLLIN | EPOLLOUT | other...) —> 判断其是否为关心时间就绪的 fd
第三层:判断 fd 是否为 listensock 。若是,... ;若不是, ...

首先,遍历类成员中 struct epoll_event _revs[gnum] 的前 n (epoll_wait的返回值) 个元素

    void handlerEvent(int num)
    {
        for (int i = 0; i < num; i++)
        {
            
        }
    }

其次,取出数组元素中的事件,并 & 关心事件(EPOLLIN | EPOLLOUT | other...) 

    void handlerEvent(int num)
    {
        for (int i = 0; i < num; i++)
        {
            //取出元素中的事件状态
            uint32_t revents = _revs[i].events;
            int sockfd = _revs[i].data.fd;
        }
    }
    void handlerEvent(int num)
    {
        for (int i = 0; i < num; i++)
        {
            uint32_t revents = _revs[i].events;
            int sockfd = _revs[i].data.fd;
            // & 关心事件
            if (revents & EPOLLIN)
            {
                
            }
        }
    }

最后,根据是否为 listen 套接字进行相对应的操作,其实因为之前我们将 listen 套接字设置为了关心读状态,所以这里可能与 echo_server 关心的事件有冲突,当需要执行另外某种操作时,可能不再存在这一步。

当套接字为监听套接字时,就可以对到来的请求进行 accept ,正式完成三次握手;当套接字不是监听套接字时,就可以根据服务端的目的执行其他操作。

    void handlerEvent(int num)
    {
        for (int i = 0; i < num; i++)
        {
            uint32_t revents = _revs[i].events;
            int sockfd = _revs[i].data.fd;
            // & 关心事件
            if (revents & EPOLLIN)
            {
                //监听套接字->执行accept,接收到来的"客户端"
                if (sockfd == _listensock->SockFd())
                {
                    InetAddr clientaddr;
                    int newfd = _listensock->Accepter(&clientaddr); 
                    if (newfd < 0)
                        continue;

                    struct epoll_event ev;
                    ev.events = EPOLLIN;
                    ev.data.fd = newfd;
                    epoll_ctl(_epfd, EPOLL_CTL_ADD, newfd, &ev);
                    LOG(DEBUG, "_listensock ready, accept done, epoll_ctl done, newfd is: %d\n", newfd);
                }
                //其他套接字->执行echo_server
                else
                {
                    char buffer[1024];
                    ssize_t n = ::recv(sockfd, buffer, sizeof(buffer), 0);
                    if (n > 0)
                    {
                        LOG(DEBUG, "normal fd %d ready, recv begin...\n", sockfd);
                        buffer[n] = 0;
                        std::cout << "client say# " << buffer << std::endl;

                        std::string echo_string = "server echo# ";
                        echo_string += buffer;
                        ::send(sockfd, echo_string.c_str(), echo_string.size(), 0);
                    }
                    else if (n == 0)
                    {
                        LOG(DEBUG, "normal fd %d close, me too!\n", sockfd);
                        ::epoll_ctl(_epfd, EPOLL_CTL_DEL, sockfd, nullptr); 
                        ::close(sockfd);
                    }
                    else
                    {
                        ::epoll_ctl(_epfd, EPOLL_CTL_DEL, sockfd, nullptr); 
                        ::close(sockfd);
                    }
                }
            }
        }
    }

四、epoll 的优点

  1. 接口使用方便: 虽然拆分成了三个函数,但是反而使用起来更方便高效。不需要每次循环都设置关注的文件描述符,也做到了输入输出参数分离
  2. 数据拷贝轻量:只在合适的时候调用 EPOLL_CTL_ADD 将文件描述符结构拷贝到内核中,这个操作并不频繁 (而 select/poll 都是每次循环都要进行拷贝)
  3. 事件回调机制:避免使用遍历,而是使用回调函数的方式,将就绪的文件描述符结构加入到就绪队列中,epoll_wait 返回直接访问就绪队列就知道哪些文件描述符就绪。这个操作时间复杂度 O(1)。即使文件描述符数目很多,效率也不会受到影响。
  4. 没有数量限制: 文件描述符数目无上限。

但是, epoll 解决不了数据拷贝的问题,这是 select/poll/epoll 都具有的特点。

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

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

相关文章

大数据-64 Kafka 高级特性 分区 分区重新分配 实测

点一下关注吧&#xff01;&#xff01;&#xff01;非常感谢&#xff01;&#xff01;持续更新&#xff01;&#xff01;&#xff01; 目前已经更新到了&#xff1a; Hadoop&#xff08;已更完&#xff09;HDFS&#xff08;已更完&#xff09;MapReduce&#xff08;已更完&am…

使用uwsgi部署Flask应用

前言&#xff1a;本人大四&#xff0c;研0&#xff0c;在24年暑假在杭州某互联网公司实习&#xff0c;本文用来记录自己在公司学到的东西。 uwsgi&#xff1a;uWSGI是一个Web服务器&#xff0c;它实现了WSGI协议、uwsgi、http等协议。Nginx中HttpUwsgiModule的作用是与uWSGI服…

集合的基本使用

数据和集合的区别 数组可以存储基本数据类型和引用数据类型。 但是&#xff0c;集合不可以直接存储基本数据类型&#xff0c;需要以包装类的方式进行存储&#xff0c;其可以存储引用数据类型。 ArrayList 成员方法 import java.util.ArrayList;/*** ClassName Test* author …

[Meachines] [Easy] Postman redis未授权访问-SSH公钥注入+RSA私钥解密+Webmin-RCE权限提升

信息收集 IP AddressOpening Ports10.10.10.160TCP:22,80,6379,10000 $ nmap -p- 10.10.10.160 --min-rate 1000 -sC -sV PORT STATE SERVICE VERSION 22/tcp open ssh OpenSSH 7.6p1 Ubuntu 4ubuntu0.3 (Ubuntu Linux; protocol 2.0) | ssh-hostkey: | 2048…

计算机基础(Windows 10+Office 2016)教程 —— 第7章 演示文稿软件PowerPoint 2016

第7章 演示文稿软件PowerPoint 2016 7.1 PowerPoint 2016入门7.1.1 PowerPoint 2016 简介7.1.2 PowerPoint 2016 的操作界面组成7.1.3 PowerPoint 2016 的窗口视图方式7.1.4 PowerPoint 2016 的演示文稿及其操作7.1.5 PowerPoint 2016 的幻灯片及其操作 7.2 演示文稿的编辑与设…

win 10 局域网共享

1&#xff0c;打开共享 控制面板\网络和 Internet\网络和共享中心\高级共享设置 &#xff08;在控制面板界面建议使用大图片或小图标容易找到目标&#xff09; 或者直接复制红色部分&#xff0c;然后打开此电脑&#xff0c;粘贴到地址栏直接回车即可直接到达几面 打开如下2个…

HCIP-综合实验

实验题目与要求如下所示&#xff1a; 根据题目要求进行划分网段&#xff1a; 1、配置各个路由器的ip地址以及环回地址&#xff1a; R1 R2 R3 R4 R5 R6 R7 2、配置rip的基本功能&#xff1a;启动rip进程&#xff0c;指定网段使用rip R1 R2 R3 R4 R5 R6 R7 3、R1创建环回172.…

微信答题小程序产品研发-页面交互设计

答题小程序页面交互设计&#xff0c;针对答题小程序的所有主要页面和功能模块&#xff0c;包括首页、轮播图、公告、微信授权登录、题库练习、出题考试、错题集、答题历史、收藏、个人中心等。 1、目的 答题小程序页面为什么要进行交互设计&#xff1f; 据我分析&#xff0c;…

品牌热度维系策略:深度触达,让每一次互动都成为爱的记忆累积

在浩瀚的商业宇宙中&#xff0c;每一个品牌都如同夜空中闪烁的星辰&#xff0c;它们或明或暗&#xff0c;共同编织着市场的繁星图景。当谈及品牌与消费者之间那份微妙而深邃的情感联结时&#xff0c;我们不禁要深入探讨一个核心议题&#xff1a;在快速迭代的消费时代&#xff0…

二十天刷leetcode【hot100】算法- day1[后端golang]

哈希表 其他语言版本 ts-day1 js-day1 python-day1 1. 两数之和 给定一个整数数组 nums 和一个整数目标值 target&#xff0c;请你在该数组中找出 和为目标值 target 的那 两个 整数&#xff0c;并返回它们的数组下标。 你可以假设每种输入只会对应一个答案。但是&#xf…

ue5 打开关卡,未正常加载地形及物品。资产必须放在content目录下

1.资产必须放在content目录下 2.要把A拿到B去用&#xff0c;必须使用资产迁移&#xff0c;不可直接拷贝&#xff0c;因为有依赖文件

Qt 学习第四天:信号和槽机制(核心特征)

信号和槽的简介 信号和插槽用于对象之间的通信。信号和插槽机制是Qt的核心特征&#xff0c;可能是不同的部分大部分来自其他框架提供的特性。信号和槽是由Qt的元对象系统实现的。介绍&#xff08;来自Qt帮助文档Signals & Slots&#xff09; 在GUI编程中&#xff0c;当我们…

安装sqllab靶机之后,练习关卡报403 forbidden

解决办法&#xff1a; 在nginx的conf文件中添加上访问index.php vim /usr/local/nginx/conf/nginx.conf 保存退出 再重启一下nginx&#xff0c;就完成了。 ./nginx -s reload

中国AI大模型场景探索及产业应用调研报告

AI大模型发展态势 定义 AI大模型是指在机器学习和深度学习领域中&#xff0c;采用大规模参数(至少在一亿个以上)的神经网络模型&#xff0c;AI大模型在训练过程中需要使用大量的算力和高质量的数据资源。 产业规模 2023年&#xff0c;中国大模型市场规模为147亿。结合《202…

python pip怎么安装包

按WinR键打开运行窗口&#xff0c;输入“cmd”&#xff0c;再按回车键&#xff0c;打开命令行窗口。 找到pip安装路径。 Python2/Python3安装路径是相同的&#xff0c;都在x:\Python xx\Scripts路径下。 拖动pip主应用程序到命令行窗口。 输入“install 模块/包名”&#xff…

Ubuntu22.04之VirtualBox7.0.18安装Win10无法启用增强功能问题(二百六十五)

简介&#xff1a; CSDN博客专家&#xff0c;专注Android/Linux系统&#xff0c;分享多mic语音方案、音视频、编解码等技术&#xff0c;与大家一起成长&#xff01; 新书发布&#xff1a;《Android系统多媒体进阶实战》&#x1f680; 优质专栏&#xff1a; Audio工程师进阶系列…

优盘驱动器未格式化:数据拯救行动指南

优盘困境&#xff1a;驱动器未格式化的挑战 在日常的数据存储与传输中&#xff0c;优盘以其便携性和高容量成为了我们不可或缺的伙伴。然而&#xff0c;当您尝试访问优盘时&#xff0c;突然弹出的“驱动器未被格式化”提示却如同晴天霹雳&#xff0c;让人措手不及。这一状况不…

AI写作|告诉你CoT的几个简单步骤,使用后大幅提升AI的推理能力

随着对 AI 不断了解&#xff0c;我深切体会到一个高效的提示词有多重要。希望这篇能给不会写高效提示词的朋友一些思路&#xff0c;让你能更顺手地使用 AI。 什么是思考链 (CoT) 思考链&#xff08;Chain of Thought&#xff0c;简称 CoT&#xff09;是一种推理策略&#xff0c…

Git代码冲突怎么处理?

我们在使用Git时难免会出现代码冲突的问题&#xff0c;出现冲突的原因是因为当本地文件的版本与目标分支中文件的版本不一致时当存在同一行的内容不同时在进行合并时会出现冲突。 代码冲突一般发生在以下情况: 1、多个分支向主分支合并时 2、同一个分支下pull或push操作时。 为…

python 可视化探索(四):电商数据可视化案例

总结&#xff1a;本文为和鲸python 可视化探索训练营资料整理而来&#xff0c;加入了自己的理解&#xff08;by GPT4o&#xff09; 注意跟练题目3中提到的多种数据替换方式&#xff0c;非常值得学习&#xff01;&#xff01; 原作者&#xff1a;作者&#xff1a;大话数据分析…