io多路复用:epoll水平触发(LT)和边沿触发(ET)的区别和优缺点

在进行ET模式的正式分析之前,我们来举个例子简单地了解下ET和LT:

假设我们通过fork函数创建了父子两个进程,并通过匿名管道来通信,在子进程中,我们一次向管道写入10个字符数据,为"aaaa\nbbbb\n";每隔5s写入10个字符数据;在父进程中,我们从管道中一次读取5个字符数据:

若我们采用的是LT模式,则在5s的周期内,会先读取5个字符数据,读完之后,因为文件描述符中仍然有数据,epoll_wait会立即返回,会继续读取接下来的5个数据,之后在5s周期以内的剩余时间内,管道中的数据都为空,如下图1。

若我们采用的是ET模式,则当父进程先读完管道中的5个字符后,由于子进程没有立即向管道中写入字符(间隔5s后才会第二次写入),所以此时父进程会先读到5个字符"aaaa\n",隔5s之后,再读到5个字符"bbbb\n",如下图2,可以看到ET模式下,随时间推移,管道中数据会越来越多。

图1

图2

epoll工作流程

想要了解ET模式和LT模式的区别,首先需要熟悉epoll的工作流程:

【注:】上图的poll不要理解成和select相似那个poll,这是通过epoll_ctl调用的,进行回调函数注册。

下面简要分析一下epoll的工作过程:

1)epoll_wait调用ep_poll,当rdlist为空(无就绪fd)时挂起当前进程,直到rdlist不空时进程才被唤醒。

2)****文件fd状态改变(buffer由不可读变为可读或由不可写变为可写),导致相应fd上的回调函数ep_poll_callback()被调用。

ep_poll_callback将相应fd对应epitem加入rdlist,导致rdlist不空,进程被唤醒,epoll_wait得以继续执行。

3)ep_events_transfer函数将rdlist中的epitem拷贝到txlist中,并将rdlist清空。

4)ep_send_events函数(很关键),它扫描txlist中的每个epitem,调用其关联fd对应的poll方法(图中蓝线)。此时对poll的调用仅仅是取得fd上较新的events(防止之前events被更新),之后将取得的events和相应的fd发送到用户空间(封装在struct epoll_event,从epoll_wait返回)。

5)之后如果这个epitem对应的fd是LT模式监听且取得的events是用户所关心的,则将其重新加入回rdlist(图中蓝线),否则(ET模式)不在加入rdlist。

ET模式 vs LT模式

区别

通过上图epoll的工作流程描述,可以看出ET模式和LT模式的区别主要发生在第5步:是否将文件描述符fd放回rdlist中,而rdlist的是否为空决定了epoll_wait函数的阻塞和非阻塞。因此,总结下二者的区别:

ET

如果是ET, epitem是不会再进入到readly list;除非fd再次发生了状态改变, ep_poll_callback被调用。【因此,ET模式可减少epoll_wait函数的调用,从而减少系统开销,提高效率。】

LT

如果是非ET, 不管你还有没有有效的事件或者数据,都会被重新插入到ready list, 再下一次epoll_wait时, 会立即返回, 并通知给用户空间。当然如果这个被监听的fds确实没事件也没数据了, epoll_wait会返回一个0,空转一次。

加入rdlist途径分析

红线:fd状态改变才会触发,那什么情况会导致fd状态的改变呢?

对于读取操作:

1)当buffer由不可读状态变为可读的时候,即由空变为不空的时候。

2)当有新数据到达时,即buffer中的待读内容变多的时候。

对于写操作:

1)当buffer由不可写变为可写的时候,即由满状态变为不满状态的时候。

2)当有旧数据被发送走时,即buffer中待写的内容变少得时候。

蓝线:fd的events中有相应的时间(位置1)即会触发。那什么情况下会改变events的相应位呢?

对于读操作:

1)buffer中有数据可读的时候,即buffer不空的时候,fd的events的可读为就置1。

对于写操作:

2) buffer中有空间可写的时候,即buffer不满的时候fd的events的可写位就置1。

【说明:】

  • 红线是时间驱动被动触发(fd状态改变);

  • 蓝线是函数查询主动触发(LT和ET判断是否加入rdlist)。

需要C/C++ Linux服务器架构师学习资料加qun579733396获取(资料包括C/C++,Linux,golang技术,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK,ffmpeg等),免费分享

代码实践

基础代码

以下为一个epoll触发模式测试的基础代码,也不算太长,直接拿来就可以测试:

#include <sys/socket.h> //for socket
#include <arpa/inet.h>  //for htonl htons
#include <sys/epoll.h>  //for epoll_ctl
#include <unistd.h>     //for close
#include <fcntl.h>      //for fcntl
#include <errno.h>      //for errno
#include <iostream>     //for coutclass fd_object
{
public:fd_object(int fd) { listen_fd = fd; }~fd_object() { close(listen_fd); }
private:int listen_fd;
};/*
./epoll for lt mode
and
./epoll 1 for et mode
*/
int main(int argc, char* argv[])
{//create a socket fdint listen_fd = socket(AF_INET, SOCK_STREAM, 0);if (listen_fd == -1){std::cout << "create listen socket fd error." << std::endl;return -1;}fd_object obj(listen_fd);//set socket to non-blockint socket_flag = fcntl(listen_fd, F_GETFL, 0);socket_flag |= O_NONBLOCK;if (fcntl(listen_fd, F_SETFL, socket_flag) == -1){std::cout << "set listen fd to nonblock error." << std::endl;return -1;}//init server bind infoint port = 51741;struct sockaddr_in bind_addr;bind_addr.sin_family = AF_INET;bind_addr.sin_addr.s_addr = htonl(INADDR_ANY);bind_addr.sin_port = htons(port);if (bind(listen_fd, (struct sockaddr *)&bind_addr, sizeof(bind_addr)) == -1){std::cout << "bind listen socket fd error." << std::endl;return -1;}//start listenif (listen(listen_fd, SOMAXCONN) == -1){std::cout << "listen error." << std::endl;return -1;}elsestd::cout << "start server at port [" << port << "] with [" << (argc <= 1 ? "LT" : "ET") << "] mode." << std::endl;//create a epoll fdint epoll_fd = epoll_create(88);if (epoll_fd == -1){std::cout << "create a epoll fd error." << std::endl;return -1;}epoll_event listen_fd_event;listen_fd_event.data.fd = listen_fd;listen_fd_event.events = EPOLLIN;if (argc > 1) listen_fd_event.events |= EPOLLET;//add epoll event for listen fdif (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, listen_fd, &listen_fd_event) == -1){std::cout << "epoll ctl error." << std::endl;return -1;}while (true){epoll_event epoll_events[1024];int n = epoll_wait(epoll_fd, epoll_events, 1024, 1000);if (n < 0)break;else if (n == 0) //timeoutcontinue;for (int i = 0; i < n; ++i){if (epoll_events[i].events & EPOLLIN)//trigger read event{if (epoll_events[i].data.fd == listen_fd){//accept a new connectionstruct sockaddr_in client_addr;socklen_t client_addr_len = sizeof(client_addr);int client_fd = accept(listen_fd, (struct sockaddr*)&client_addr, &client_addr_len);if (client_fd == -1)continue;socket_flag = fcntl(client_fd, F_GETFL, 0);socket_flag |= O_NONBLOCK;if (fcntl(client_fd, F_SETFL, socket_flag) == -1){close(client_fd);std::cout << "set client fd to non-block error." << std::endl;continue;}epoll_event client_fd_event;client_fd_event.data.fd = client_fd;client_fd_event.events = EPOLLIN | EPOLLOUT;if (argc > 1) client_fd_event.events |= EPOLLET;if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, client_fd, &client_fd_event) == -1){std::cout << "add client fd to epoll fd error." << std::endl;close(client_fd);continue;}std::cout << "accept a new client fd [" << client_fd << "]." << std::endl;}else{std::cout << "EPOLLIN event triggered for client fd [" << epoll_events[i].data.fd << "]." << std::endl;char recvbuf[1024] = { 0 };int m = recv(epoll_events[i].data.fd, recvbuf, 1, 0); // only read 1 bytes when read event triggeredif (m == 0 || (m < 0 && errno != EWOULDBLOCK && errno != EINTR)){if (epoll_ctl(epoll_fd, EPOLL_CTL_DEL, epoll_events[i].data.fd, NULL) != -1)std::cout << "the client fd [" << epoll_events[i].data.fd << "] disconnected." <<  std::endl;close(epoll_events[i].data.fd);}std::cout << "recv data from client fd [" << epoll_events[i].data.fd << "] and data is [" << recvbuf << "]." << std::endl;}}else if (epoll_events[i].events & EPOLLOUT){if (epoll_events[i].data.fd == listen_fd) //trigger write eventcontinue;std::cout << "EPOLLOUT event triggered for client fd [" << epoll_events[i].data.fd << "]." << std::endl;}}}return 0;
}

简单说下这段代码的测试方法,可以使用g++ testepoll.cpp -o epoll进行编译,编译后通过./epoll运行为LT模式,通过./epoll et模式运行为ET模式,我们用编译好的epoll程序作为服务器,使用nc命令来模拟一个客户端。

测试分类

1.编译后直接./epoll,然后在另一个命令行窗口用 nc -v 127.0.0.1 51741 命令模拟一次连接,此时 ./epoll 会产生大量的 EPOLLOUT event triggered for client fd ...,那是因为在LT模式下,EPOLLOUT会被一直触发。

albert@home-pc:/mnt/d/data/cpp/testepoll$ ./epoll
start server at port [51741] with [LT] mode.
accept a new client fd [5].
EPOLLOUT event triggered for client fd [5].
EPOLLOUT event triggered for client fd [5].
EPOLLOUT event triggered for client fd [5].
EPOLLOUT event triggered for client fd [5].
EPOLLOUT event triggered for client fd [5].
EPOLLOUT event triggered for client fd [5].
EPOLLOUT event triggered for client fd [5].
EPOLLOUT event triggered for client fd [5].
...

2.注释包含 EPOLLOUT event triggered for client fd 输出内容的第152行代码,编译后 ./epoll运行,然后在另一个命令行窗口用 nc -v 127.0.0.1 51741 模拟一次连接后,输入abcd回车,可以看到服务器./epoll输出内容,EPOLLIN被触发多次,每次读取一个字节。

albert@home-pc:/mnt/d/data/cpp/testepoll$ ./epoll
start server at port [51741] with [LT] mode.
accept a new client fd [5].
EPOLLIN event triggered for client fd [5].
recv data from client fd [5] and data is [a].
EPOLLIN event triggered for client fd [5].
recv data from client fd [5] and data is [b].
EPOLLIN event triggered for client fd [5].
recv data from client fd [5] and data is [c].
EPOLLIN event triggered for client fd [5].
recv data from client fd [5] and data is [d].
EPOLLIN event triggered for client fd [5].
recv data from client fd [5] and data is [
].

3.还原刚才注释的那行代码,编译后执行 ./epoll et 启动服务器,然后在另一个命令行窗口用 nc -v 127.0.0.1 51741 模拟一次连接后,然后在另一个命令行窗口用 nc -v 127.0.0.1 51741 模拟一次连接,服务器窗口显示触发了EPOLLOUT事件

albert@home-pc:/mnt/d/data/cpp/testepoll$ ./epoll et
start server at port [51741] with [ET] mode.
accept a new client fd [5].
EPOLLOUT event triggered for client fd [5].

在此基础上,从刚刚运行nc命令的窗口中输入回车、输入回车、输出回车,那么epoll服务器窗口看到的是触发了三次EPOLLIN事件,每次收到一个回车:

albert@home-pc:/mnt/d/data/cpp/testepoll$ ./epoll et
start server at port [51741] with [ET] mode.
accept a new client fd [5].
EPOLLOUT event triggered for client fd [5].
EPOLLIN event triggered for client fd [5].
recv data from client fd [5] and data is [
].
EPOLLIN event triggered for client fd [5].
recv data from client fd [5] and data is [
].
EPOLLIN event triggered for client fd [5].
recv data from client fd [5] and data is [
].

但是如果在nc模拟的客户端里输出abcd回车,那么在epoll服务器窗口触发一次EPOLLIN事件接收到一个a之后便再也不会触发EPOLLIN了,即使你在nc客户端在此输入也没有用,那是因为在接受的缓冲区中一直还有数据,新数据来时没有出现缓冲区从空到有数据的情况,所以在ET模式下也注意这种情况。

albert@home-pc:/mnt/d/data/cpp/testepoll$ ./epoll et
start server at port [51741] with [ET] mode.
accept a new client fd [5].
EPOLLOUT event triggered for client fd [5].
EPOLLIN event triggered for client fd [5].
recv data from client fd [5] and data is [
].
EPOLLIN event triggered for client fd [5].
recv data from client fd [5] and data is [
].
EPOLLIN event triggered for client fd [5].
recv data from client fd [5] and data is [
].
EPOLLIN event triggered for client fd [5].
recv data from client fd [5] and data is [a].

怎么解决ET触发了一次就不再触发了

改代码呗,ET模式在连接后触发一次EPOLLOUT,接收到数据时触发一次EPOLLIN,如果数据没收完,以后这两个事件就再也不会被触发了,要想改变这种情况可以再次注册一下这两个事件,时机可以选择接收到数据的时候,所以可以修改这部分代码:

else
{std::cout << "EPOLLIN event triggered for client fd [" << epoll_events[i].data.fd << "]." << std::endl;char recvbuf[1024] = { 0 };int m = recv(epoll_events[i].data.fd, recvbuf, 1, 0); // only read 1 bytes when read event triggeredif (m == 0 || (m < 0 && errno != EWOULDBLOCK && errno != EINTR)){if (epoll_ctl(epoll_fd, EPOLL_CTL_DEL, epoll_events[i].data.fd, NULL) != -1)std::cout << "the client fd [" << epoll_events[i].data.fd << "] disconnected." <<  std::endl;close(epoll_events[i].data.fd);}std::cout << "recv data from client fd [" << epoll_events[i].data.fd << "] and data is [" << recvbuf << "]." << std::endl;
}

添加再次注册的逻辑:

else
{std::cout << "EPOLLIN event triggered for client fd [" << epoll_events[i].data.fd << "]." << std::endl;char recvbuf[1024] = { 0 };int m = recv(epoll_events[i].data.fd, recvbuf, 1, 0); // only read 1 bytes when read event triggeredif (m == 0 || (m < 0 && errno != EWOULDBLOCK && errno != EINTR)){if (epoll_ctl(epoll_fd, EPOLL_CTL_DEL, epoll_events[i].data.fd, NULL) != -1)std::cout << "the client fd [" << epoll_events[i].data.fd << "] disconnected." <<  std::endl;close(epoll_events[i].data.fd);}epoll_event client_fd_event;client_fd_event.data.fd = epoll_events[i].data.fd;client_fd_event.events = EPOLLIN | EPOLLOUT;if (argc > 1) client_fd_event.events |= EPOLLET;epoll_ctl(epoll_fd, EPOLL_CTL_MOD, epoll_events[i].data.fd, &client_fd_event);std::cout << "recv data from client fd [" << epoll_events[i].data.fd << "] and data is [" << recvbuf << "]." << std::endl;
}

这次以./epoll et方式启动服务器,使用nc -v 127.0.0.1 51741模拟客户端,输入abc回车发现,epoll服务器输出显示触发的事件变了:

albert@home-pc:/mnt/d/data/cpp/testepoll$ ./epoll et
start server at port [51741] with [ET] mode.
accept a new client fd [5].
EPOLLOUT event triggered for client fd [5].
EPOLLIN event triggered for client fd [5].
recv data from client fd [5] and data is [a].
EPOLLIN event triggered for client fd [5].
recv data from client fd [5] and data is [b].
EPOLLIN event triggered for client fd [5].
recv data from client fd [5] and data is [c].
EPOLLIN event triggered for client fd [5].
recv data from client fd [5] and data is [
].
EPOLLOUT event triggered for client fd [5].

为什么使用边缘触发必须使用非阻塞

在设置边缘触发时,因为每次发消息只会触发一次(不管缓存区是否还留有数据),所以必须把数据一次性读取出来,否则会影响下一次消息

下面的代码实现的是监听文件描述符,每次固定读取5个字节

先看下面这段代码

int main(int argc, char *argv[])
{int epfd, nfds;int i;struct epoll_event event, events[10];char buf[5];int flag;epfd = epoll_create(10);event.data.fd = 0; /* 监听标准输入 */event.events = EPOLLIN | EPOLLET; /* 读监听、边缘触发 *///event.events = EPOLLIN; /* 读监听、边缘触发 */epoll_ctl(epfd, EPOLL_CTL_ADD, 0, &event);#if 0flag = fcntl(0, F_GETFL);fcntl(0, F_SETFL, flag | O_NONBLOCK);
#endif	while (1){int i;int num = 0;char c;nfds = epoll_wait(epfd, events, 5, -1); /* 返回就绪的文件描述符 */for (i = 0; i < nfds; ++i) {if (events[i].data.fd == STDIN_FILENO) {for(i = 0; i < 5; i++){buf[i] = getc(stdin);if(buf[i] == -1)break;}printf("hello world\n");}}}return 0;
}

这段代码的目的是,使用边沿触发,每次触发读取5个字节,此时getc函数是阻塞读取,这就会引起一个问题,当缓存中的数据小于5时,就会在这里阻塞等待,导致无法处理其他IO,这是非常错误的行为,在并发的服务器中,会导致服务器阻塞,无法处理其他客户端

正确的处理方法是将读取的文件描述符设置为非阻塞,循环读取,如果没有数据了就放回错误,这样就不会让服务器阻塞

可以添加下面这两行代码,将标准输入设置为非阻塞IO

flag = fcntl(0, F_GETFL);
fcntl(0, F_SETFL, flag | O_NONBLOCK);

这就解释了为什么边沿触发必须使用非阻塞的问题。

水平触发和边沿触发的优缺点

水平触发LT:

优点:程序简单,会完整地读取所有数据。

缺点:重复地事件触发会影响高并发服务器地性能,因为epoll监控事件涉及到系统调用,需要用户态-内核态的转换。LT消耗了大量的系统资源,影响服务器性能;

边沿触发ET:

优点:每次epoll_wait只用触发一次,通过程序逻辑实现读取缓冲区的所有数据,工作效率高,大大提升了服务器性能;

缺点(没归纳出来,随便写一个):不能保证数据的完整。(这个可以通过上面提到的程序逻辑实现完整地读取数据)

边沿触发没什么缺点?那是不是用epoll就用ET边沿触发就好了?

我的理解是,YES。在日常用epoll实现并发处理,可以优先使用“边沿触发(EPOLLET)+非阻塞IO”模式。

在高并发服务器中边沿触发(ET)的效率更高

因为边沿触发只在数据到来的一刻才触发,很多时候服务器在接受大量数据时会先接受数据头部(水平触发在此触发第一次,边沿触发第一次)。

接着服务器通过解析头部决定要不要接这个数据。此时,如果不接受数据,水平触发需要手动清除(水平触发当有数据时,会一直触发,直到没有数据可读),而边沿触发可以将清除工作交给一个定时的清除程序去做(只触发一次,不需要的数据可以不读),自己立刻返回。

但是如果sockfd中发送的数据较小,我一次recv就能全部读完,这样LT也不会重复触发epoll事件,和边沿触发的性能差不多,那我们为什么不用更简单的水平触发呢,当然可以使用。那其实就可以理解水平触发和边沿触发是有一个分界点,就是看sockfd的数据是小数据还是大数据。recv的BUFFER_LENGTH如果一次能接收完recv buffer中的数据,就是小数据,一次接收不完就是大数据。小数据就用水平触发,大数据就用边沿触发

但但但但是LT还在一种场景有使用,就是Nginx服务器中listenfd是用的水平触发 。(网络服务器中一般涉及两类sockfd,一种是用于监听是否有连接请求的socket,一种是用于传输数据的socket(上面讲的sockfd都是用于传输数据的))

有一些解释是listenfd用水平触发,如果多个client同时连接进来,listenfd里面积攒多个连接的话,accept一次只处理一个连接,防止漏掉连接,选择水平触发。

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

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

相关文章

SmartX 分布式存储产品全新升级,支持文件存储能力与纠删码机制

近日&#xff0c;SmartX 正式发布了 SMTX ZBS 5.6 版本&#xff0c;通过引入对文件存储的支持能力&#xff0c;可作为企业统一存储平台&#xff0c;为大规模虚拟化、私有云、容器等环境提供高可靠、高可用、高性能、易扩展的企业级分布式块存储和分布式文件存储服务。该版本还引…

2024 年至今回顾:The Sandbox 创作者的历程及下一步展望

2024 年上半年是 The Sandbox 令人振奋的旅程&#xff01;从激动人心的里程碑、丰厚的奖励到创新的功能&#xff0c;我们见证了来自充满活力的社区的惊人创造力。 作为平台的生命线&#xff0c;我们致力于帮助创作者发光发热。让我们深入了解过去六个月中最激动人心的时刻和更…

OpenHarmony(鸿蒙南向开发)——标准系统方案之瑞芯微RK3566移植案例(下)

往期知识点记录&#xff1a; 鸿蒙&#xff08;HarmonyOS&#xff09;应用层开发&#xff08;北向&#xff09;知识点汇总 鸿蒙&#xff08;OpenHarmony&#xff09;南向开发保姆级知识点汇总~ OpenHarmony&#xff08;鸿蒙南向开发&#xff09;——轻量系统STM32F407芯片移植案…

MySQL聚合统计和内置函数

【数据库】MySQL聚合统计 王笃笃-CSDN博客https://blog.csdn.net/wangduduniubi?typeblog显示平均工资低于2000的部门和它的平均工资 mysql> select deptno,avg(sal) deptavg from emp group by deptno; --------------------- | deptno | deptavg | --------------…

基于树表的查找

二叉排序树 相关概念 存储结构 查找 具体实现 算法分析 插入 创建 删除 无孩子 有一个孩子 有左右孩子 示例 平衡二叉树 概念 示例 插入调整 类型 由插入结点在失衡结点的位置来定&#xff1a;插入结点C为失衡结点A的左子树的左孩子&#xff0c;因此为LL型。 原则 即选择中…

10.第二阶段x86游戏实战2-反编译自己的程序加深堆栈的理解

免责声明&#xff1a;内容仅供学习参考&#xff0c;请合法利用知识&#xff0c;禁止进行违法犯罪活动&#xff01; 本次游戏没法给 内容参考于&#xff1a;微尘网络安全 工具下载&#xff1a; 链接&#xff1a;https://pan.baidu.com/s/1rEEJnt85npn7N38Ai0_F2Q?pwd6tw3 提…

[C++进阶[六]]list的相关接口模拟实现

1.前言 本章重点 在list模拟实现的过程中&#xff0c;主要是感受list的迭代器的相关实现&#xff0c;这是本节的重点和难点。 2.list接口的大致框架 list是一个双向循环链表&#xff0c;所以在实现list之前&#xff0c;要先构建一个节点类 template <class T> struct L…

Java 中 List 常用类和数据结构详解及案例示范

1. 引言 在 Java 开发中&#xff0c;List 是最常用的数据结构接口之一&#xff0c;它用于存储有序的元素集合&#xff0c;并允许通过索引进行随机访问。电商系统中&#xff0c;如购物车、订单列表和商品目录等功能都依赖 List 进行数据管理。选择适当的 List 实现类能够显著提…

C++STL~~priority_queue

文章目录 容器适配器一、priority_queue的概念二、priority_queue的使用三、priority_queue的练习四、仿函数五、总结 容器适配器 什么是适配器 适配器是一种设计模式(设计模式是一套被反复使用的、多数人知晓的、经过分类编目的、代码设计经验的总结)&#xff0c;该种模式是将…

交流电力控制电路之交流调功电路、交流电力电子开关

目录 一、交流调功电路 二、交流电力电子开关 交流调压电路可看&#xff1a;交流调压电路 交流调压电路、交流调功电路和交流电力开关的异同点&#xff1a; 一、交流调功电路 交流调功电路用于调节电力设备的功率输出&#xff0c;通过改变电路中电压、电流的有效值&#xff…

STL,智能指针和线程安全,线程安全的单例模式和懒汉饿汉的实现,以及读者写者问题

&#x1f351;个人主页&#xff1a;Jupiter. &#x1f680; 所属专栏&#xff1a;Linux从入门到进阶 欢迎大家点赞收藏评论&#x1f60a; 目录 &#x1f4da;STL&#xff0c;智能指针和线程安全 &#x1f4d5;STL中的容器是否是线程安全的?&#x1f4a1;智能指针是否是线程安全…

Threejs之看房案例(下)

本文目录 前言最终效果1、点精灵1.1 添加点精灵1.2 点精灵效果2、添加事件2.1 鼠标移动事件2.1.1 效果2.2 鼠标点击事件2.2.1 效果2.3 切换互通3. 完整代码前言 在Threejs之看房案例(上)这篇博客中我们已经完成了大厅的3d观看效果,但是我们会发现如果想去其他房间观看,没有…

使用SQL递归查询树状结构,又可以跟同事吹牛了!

前言 在关系型数据库中&#xff0c;数据通常存储为二维表格&#xff08;rows 和 columns&#xff09;。然而&#xff0c;在实际业务中&#xff0c;很多场景下我们需要处理树状结构的数据&#xff0c;例如&#xff1a; 公司组织架构&#xff1a;从某个部门开始&#xff0c;查询…

Python异常处理:自定义异常②

文章目录 1. 什么是自定义异常&#xff1f;2. 为什么需要自定义异常&#xff1f;3. 如何定义自定义异常&#xff1f;3.1 基本自定义异常3.2 带详细信息的自定义异常3.3 自定义异常的继承层次 4. 使用自定义异常4.1 抛出自定义异常4.2 捕获自定义异常 5. 自定义异常的应用场景5.…

二叉树——数据结构

这次我们来学习一下数据结构中的二叉树 1. 二叉树的概念及结构 1.1 二叉树的定义 定义&#xff1a;所有结点的度小于等于2的树。 上图中可以看出 二叉树不存在度大于2的结点二叉树的子树有左右之分&#xff0c;次序不能颠倒&#xff0c;因此二叉树是有序树。 任意二叉树都…

2024年适合培训服务企业的7款CRM盘点

培训服务行业在线索管理、客户管理、数据分析、项目管理、师资管理和课程管理等方面&#xff0c;使用CRM可以事半功倍&#xff0c;最重要的是&#xff0c;可以用数据说话&#xff0c;找到降本增效的方向。 下面对培训服务行业常用测CRM做个盘点&#xff0c;包括国内比较头部的…

米壳AI:跨境电商必备:不损失原图的图片翻译工具!

嘿&#xff0c;跨境电商的小伙伴们&#xff01; 今天来聊聊如何突破语言壁垒&#xff0c;让你的商品在国际市场上大放异彩。 随着 “一带一路” 战略的不断推进&#xff0c;跨境电商的发展势头愈发强劲。然而&#xff0c;语言障碍却成为了跨境交易中的一大难题。别担心&#x…

ppt组织结构图怎么增加分支?

在使用ppt里边的SmartArt来制作组织结构图的时候&#xff0c;我们发现里边的图形不够用&#xff0c;需要增加分支&#xff0c;这也就是大家近期问的ppt组织结构图怎么增加分支。今天设计学徒自学网小编就把具体的操作步骤分享给大家了&#xff0c;希望能帮助你们&#xff01; …

RFID技术实现消防物资消防车无感化智能管理设计方案

在消防工作中&#xff0c;物资管理的高效性与准确性直接关系到救援行动的成败&#xff0c;传统的消防物资管理方式主要依赖人工记录和定期盘点&#xff0c;这种方式存在着诸多弊端。首先&#xff0c;人工记录容易出现错误&#xff0c;数据的准确性难以保证。例如&#xff0c;在…