libevent实战:C++高并发服务器网络模型重构与性能调优
服务器并发扛不住多数情况下不是机器不够强而是网络模型从一开始就选错了。前一阵我把一个消息网关的接入层整个重写了一遍从“每连接一线程”改成基于libevent的事件驱动模型C服务器稳定保持的连接数从几百直接上了几万CPU占有率还降了下去。libevent这套库在网络编程圈子里名声很响但真正把它用得顺手的资料并不多大部分教程还停留在“能跑一个echo”的程度。这篇文章我会完整记录我从选型、编码、压测到上线踩坑的过程。内容会比较长适合已经被高并发连接、阻塞线程模型折磨过的C开发者也适合想系统了解libevent到底怎么落地的人。我会把关键代码直接贴出来每一步都讲清楚为什么这样做最后再给出一份我自己的生产配置清单。1. 为什么放弃多线程阻塞模型转向libevent事件驱动1.1 阻塞I/O的“一人一连接”困境最先崩在哪早期我做接入层用的是最传统的模型主线程accept来一个连接就开一个线程线程里阻塞地read、处理、write、再read。连接的代码写起来非常顺手逻辑完全是顺序的但随着连接数上升问题会一个接一个冒出来。首先是线程栈的内存开销。Linux默认线程栈8MB只是虚拟内存但几百上千个线程时实际物理内存和调度压力都很大。连接一多线程切换变得频繁每次上下文切换要保存恢复寄存器、栈指针、浮点状态这些开销全部变成CPU的无效消耗。再说活跃度问题——网络连接大部分人都不活跃一个维持连接、偶尔发心跳的客户端却占用着一个完整的线程和系统调度资源这在工程上是非常奢侈的。我当时遇到的现象很典型连接数到两千左右线程数两千多CPU占用率到了百分之七八百延迟抖动厉害。用ss -s看是大量ESTABLISHED连接但实际流量很低。这时候我很清楚问题不在业务代码而在“一连接一线程”这个结构本身。1.2 裸用epoll很爽但工程化细节太多了接着我尝试绕开线程模型直接用Linux的epoll。epoll本身不难核心就是epoll_create、epoll_ctl、epoll_wait三件套把关注的fd挂进去等待内核通知。但写着写着就发现裸epoll离一个能用的服务器还差得很远。你得自己维护可读写的fd集合自己解析和处理EPOLLIN、EPOLLOUT、EPOLLERR要自己管理每个fd的数据缓冲没读完的、写不出去的都要保存要自己实现定时器因为大量连接需要超时关闭、心跳检测、任务延迟执行还要考虑信号处理、fork之后的子进程事件分发。这一套全部从零写工作量很大而且很容易写出错。很多人项目里的bug不是业务逻辑而是出在了epoll的边角处理上。libevent的价值就在这里。它是C语言实现的事件库把select、poll、epoll、kqueue这些底层机制统一封装成一套API。上层写一套代码在Linux上用epoll在BSD/macOS上用kqueue不用改业务逻辑。更重要的是libevent把定时器、信号、I/O事件统一在一个事件循环里管理定时器用最小堆实现触发精度和性能都足够好。这个整合能力是它相对裸epoll最大的优势。1.3 事件驱动对程序员思维方式的真实冲击从“阻塞读”切换到“事件回调”最难的不是API而是思维模型。阻塞模型下你可以顺着写第一行等数据第二行处理第三行返回结果。事件驱动模型下你不能再假设顺序而是“socket可读了”告诉你一个事件你处理完后如果没数据了就等下一个事件。所以每条连接不能只是裸socket最好用一个会话结构把上下文串起来对端地址、读缓冲、写缓冲、状态机状态、最近活跃时间点、应用层业务字段。所有回调都通过arg参数拿到这个会话结构再根据当前状态决定怎么处理。这是我最初踩坑最狠的地方早期我想着回调里用全局map查socket对应的上下文结果每次都要加锁性能损失不小。后来改成连接建立时就把会话指针传进回调参数完全避开了查表。想用事件驱动模型就得接受“一切异步”。接收数据是异步的发送数据也是异步的——你调用写入接口只是把数据放进了发送缓冲区真正发出去是事件循环后面的事。所以发送完一批数据后不能立刻关闭连接必须等到写缓冲耗尽再关否则会出现数据丢在半路的情况。2. libevent的核心运转逻辑事件循环这条流水线到底怎么转2.1 event_base是整条流水线的引擎理解libevent第一个概念就是event_base。它是全局事件循环的调度中心一个进程里通常有一个或多个event_base每个event_base内部会持有当前的IO多路复用后端。Linux上默认是epollBSD/macOS上默认是kqueueWindows上则用IOCP的封装当然在Windows上libevent的默认后端略有差异。主流程非常简单创建base注册事件调用event_base_dispatch进入循环。循环内部做的事情大致是这样把当前所有注册的事件按类型归集把fd传给epoll_wait或者poll同时根据当前最早到期的定时器计算epoll_wait的超时时间内核返回就绪事件后逐个触发对应的回调如果没有事件到期就把循环交给下一次等待。这个设计的巧妙之处在于定时器不需要单独开线程去管。比如你有一个连接60秒没活动就超时关闭libevent会把定时器放到最小堆里epoll_wait的超时时间直接取堆顶元素的时间一个循环里同时搞定IO等待和定时检查。如果底层epoll不支持超时精度libevent自己也会做校准。这套调度机制让我想起流水线每个工位只关心自己负责的事事件来了就处理没事件就等不会出现阻塞线程在那边空转。2.2 event对象注册什么、什么时候触发、触发了干什么event是libevent里的基本事件单元。创建一个事件主要需要四个要素所属的event_base、要监听的fd、关注的事件类型、以及回调函数。常用的事件类型有EV_READ、EV_WRITE、EV_TIMEOUT、EV_SIGNAL还有一个容易被忽视的EV_PERSIST。如果一个读事件不带EV_PERSIST那么触发一次回调后事件就会自动从事件循环里移除下次想继续读还要重新event_add。这很容易让人困惑实际应用中我几乎总是带上EV_PERSIST让事件在回调执行完后自动继续挂在循环里直到我显式event_del。还有一个重点是边缘触发EV_ET。水平触发模式下只要fd还有数据没读完回调就会被反复触发边缘触发模式下当fd状态从无数据变为有数据时触发一次。边缘触发性能上限高但要求你一次把数据尽量读完否则剩余数据要等下一次新的数据到达才会触发这可能造成延迟。我的建议是新手先用默认的水平触发性能已经足够好后面再考虑边缘触发优化。2.3 bufferevent给连接读写加一条传送带直接操作event 裸socket做读写你得自己处理读一半、写不完整、缓冲累积这些麻烦事。为此libevent提供了bufferevent这个封装层它本质是一个连接级的读写缓冲内部自动处理非阻塞socket的读入和写出。bufferevent_socket_new创建一个缓冲事件绑定一个fd。之后你可以设置三个回调可读回调、可写回调、事件回调。可读回调触发时数据已经被libevent从socket读进了内部的输入缓冲evbuffer可写回调触发时表示内部输出缓冲已经写入了一部分到内核。事件回调则处理连接关闭、错误、超时等异常状态。bufferevent还有一个很有用的水位设置。读低水位表示“至少缓冲里积攒了多少字节才触发读回调”适合做分包处理——比如协议头固定8字节你可以等缓冲满8字节再开始解析。写高水位则能形成背压当输出缓冲超过高水位时libevent会停止从内核读取更多数据防止对端不消费导致本地内存无限增长。这个特性哪怕在裸epoll方案里也要自己花不少精力去实现而libevent一行代码就设置好了。3. 代码实战用libevent做一个可运行的并发TCP网关3.1 最小可编译的echo服务器带定时器心跳下面这份代码是我在实际项目基础上简化出来的完全可以直接编译运行。它的功能是监听9000端口客户端连上来后发送什么就原样返回什么同时每秒在终端打一行定时器日志。// echo_server.cppLinux环境下编译 #include event2/event.h #include event2/listener.h #include event2/bufferevent.h #include event2/buffer.h #include sys/socket.h #include netinet/in.h #include cstdio #include cstring static void on_timer(evutil_socket_t fd, short what, void *arg) { static long ticks 0; printf(timer tick %ld\n, ticks); } static void on_read(struct bufferevent *bev, void *arg) { // 直接把输入缓冲搬到输出缓冲实现echo struct evbuffer *input bufferevent_get_input(bev); struct evbuffer *output bufferevent_get_output(bev); evbuffer_add_buffer(output, input); } static void on_event(struct bufferevent *bev, short events, void *arg) { if (events (BEV_EVENT_EOF | BEV_EVENT_ERROR)) { // 对端关闭或出错释放连接 bufferevent_free(bev); } } static void on_accept(struct evconnlistener *listener, evutil_socket_t fd, struct sockaddr *addr, int socklen, void *arg) { struct event_base *base (struct event_base *)arg; struct bufferevent *bev bufferevent_socket_new(base, fd, BEV_OPT_CLOSE_ON_FREE); if (!bev) { evutil_closesocket(fd); return; } // 设置读、写、事件回调 bufferevent_setcb(bev, on_read, NULL, on_event, NULL); bufferevent_enable(bev, EV_READ | EV_WRITE); // 生产环境设置输出高水位防止对端不读导致内存膨胀 // bufferevent_setwatermark(bev, EV_WRITE, 0, 32 * 1024); } int main() { struct event_base *base event_base_new(); if (!base) { return 1; } // 每秒触发一次的定时器演示事件循环里的定时能力 struct event *timer event_new(base, -1, EV_PERSIST, on_timer, NULL); struct timeval one_sec {1, 0}; event_add(timer, one_sec); struct sockaddr_in sin; memset(sin, 0, sizeof(sin)); sin.sin_family AF_INET; sin.sin_port htons(9000); struct evconnlistener *listener evconnlistener_new_bind( base, on_accept, base, LEV_OPT_REUSEABLE | LEV_OPT_CLOSE_ON_FREE, 1024, // backlog (struct sockaddr *)sin, sizeof(sin)); if (!listener) { perror(listen failed); return 1; } printf(echo server listening on 9000\n); event_base_dispatch(base); evconnlistener_free(listener); event_base_free(base); return 0; }编译命令很简单g -O2 -o echo_server echo_server.cpp -levent运行后用任意TCP客户端测试printf hello, libevent\n | nc 127.0.0.1 9000这里有几个必须说清楚的细节。evconnlistener_new_bind是libevent提供的监听封装内部已经把accept、监听fd注册、连接到达回调都处理好了比手动创建socket再自己做event注册省事得多。LEV_OPT_REUSEABLE对应SO_REUSEADDR防止上次服务未完全退出时端口被占用。LEV_OPT_CLOSE_ON_FREE表示listener销毁时自动关闭监听socket。bufferevent_socket_new里的BEV_OPT_CLOSE_ON_FREE同样重要它保证连接对象释放时自动关闭底层socket不会泄漏fd。bufferevent_setcb(bev, on_read, NULL, on_event, NULL)里第二个参数是可写回调这里传NULL代表不需要主动处理写事件。最后一个NULL是传给所有回调的arg如果需要传递会话对象就把会话指针放在这里。定时器的用法在代码里也一并展示了event_new的fd传-1类型传EV_PERSIST添加时设置超时时间为1秒。之后这个事件会周期性触发和网络事件完全共用一个事件循环不需要额外线程。如果你只是想要一次性的延迟回调把EV_PERSIST去掉即可。3.2 多线程版本的正确姿势多个event_base而不是共享一个单线程的libevent在高并发连接上表现很好但现代服务器都是多核CPU只跑一个事件循环会白白浪费核心。libevent支持多线程但正确的做法不是让多个线程在一个event_base上同时跑event_base_loop——一个base的内部数据结构本身不是完全线程安全的强行多线程共享base要么用内部锁把性能拖垮要么回调里出现严重的数据竞争。我实际采用的结构是主线程运行一个listener负责accept新连接。连接建立后按照某种负载均衡策略最简单的就是连接数取模分发给N个工作线程。每个工作线程拥有自己独立的event_base、自己独立的事件循环连接被哪个线程负责之后就一直在那个线程里处理不再换人。跨线程通知是必须解决的问题。一个线程accept到新连接后怎么让另一个线程的事件循环知道“你有一批新连接要接手”libevent自身没有直接支持“跨线程唤醒base”但标准做法是每个工作线程创建一个socketpair或eventfd在base里注册为读事件。需要唤醒对应线程时往管道里写一个字节事件循环就会立刻被唤醒然后从队列里取出新连接并处理。我线上项目也是这么做的效果很稳定。多base结构下还要注意一点不要直接在事件回调里加全局锁去访问共享数据。如果多个工作线程需要共享某些业务状态尽量把状态拆分到线程本地或者用无锁队列 每线程事件归集的方式同步否则锁竞争会把事件驱动的性能优势全部抵消。3.3 本地压测最容易忽略的两件事写完echo服务器后我习惯先用脚本做一轮冒烟测试。最常见的测试方式是直接写一个多线程的Python脚本同时创建几千个连接有些连接发几条消息后保持不动有些连接持续收发模拟真实场景。import socket, threading, time, random HOST, PORT 127.0.0.1, 9000 def one_conn(idx): s socket.create_connection((HOST, PORT), timeout10) s.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) for _ in range(200): data bx * random.randint(64, 1024) s.sendall(data) s.recv(len(data)) time.sleep(0.001) s.close() threads [] for i in range(2000): t threading.Thread(targetone_conn, args(i,)) threads.append(t) t.start() for t in threads: t.join() print(done)跑这个脚本时有两个最容易忽略的点。第一测试机的文件描述符上限。默认ulimit通常是1024几千个连接根本建不起来跑之前先执行ulimit -n 100000或者改/etc/security/limits.conf。第二本机压测时网络往返延迟几乎为零客户端本身反而最容易成为瓶颈。如果压测结果上不去先确认客户端是不是把线程开到了足够多或者改用多个压测机同时打否则你会误判成服务器不行。4. 实测数据与性能复盘这台“高并发”服务器到底能扛多少4.1 测试环境和基准定义我在一台8核16G内存的Linux主机上做了压测内核版本5.4libevent版本2.1.12服务端编译参数-O2测试用的就是上一节的echo server单进程单线程版本。连接类型是随时可能发消息的长连接消息大小随机在64到1024字节之间。这里要说明一下echo服务器是最理想的服务器模型因为它几乎不涉及复杂业务逻辑场景纯粹测出来的数据反映的是libevent事件循环本身的吞吐上限。如果你用它来估算真实业务系统的容量必须在结果上乘一个业务复杂度系数。4.2 压测结果连接数、CPU、内存、延迟并发连接数单核CPU占用内存占用估算平均RT本机现象1000约30%约50MB0.3ms非常稳10000约70%约450MB0.8ms稳定无掉线50000单核接近满载约2GB1.5ms建立连接变慢稳定后仍工作阻塞模型5000400%以上多核线程栈占用极大几十ms到秒级延迟抖动严重单进程单线程的libevent在50000连接、持续读写的压力下能把一个CPU核心跑满但系统整体还能继续服务。这里最让我感慨的是内存差异阻塞模型5000连接就要五千个线程栈加上线程控制块和调度数据内存和CPU都消耗巨大libevent这边每个连接只是一小块会话结构几KB到几十KB50000连接的资源占用还在可控范围内。如果你需要用到多核切到3.2说的多base结构后吞吐量基本可以按核数线性扩展。我当时的实测是4个工作线程压测机到60000连接四核都被有效利用整体QPS比单线程高了三倍以上没有出现明显的扩展瓶颈。4.3 真相性能瓶颈往往不在libevent本身压测过后我做了很多轮调优最终意识到一个事实在高连接数场景下真正限制吞吐量的往往不是事件循环本身而是系统调用和业务处理。epoll_wait返回后要逐个处理就绪事件每处理一个连接就至少涉及一次read和一次write这就是两次系统调用。echo模型下数据从网卡到用户态、从用户态回网卡中间还要经过libevent的evbuffer拷贝这部分开销在数十万QPS时非常可观。其次accept新连接本身也有代价。当连接堆积在backlog里evconnlistener每次回调只accept一个连接高并发下建立连接可能变慢必要时需要自己循环accept直到EAGAIN。还有一个容易被忽略的瓶颈是内核参数。高连接数下如果fs.file-max、net.ipv4.ip_local_port_range、net.core.somaxconn没有调大服务端会直接出现 accept 失败或者正常连接进不了backlog的情况。调优服务器性能不是只调代码内核网络参数同样重要。5. 上线前必须处理的六个细节连接杂症与内核调优5.1 TIME_WAIT和CLOSE_WAIT根本不是一回事高并发服务器的socket状态我最关注两个TIME_WAIT和CLOSE_WAIT。这两个状态常被混为一谈但含义和应对方法完全不同。TIME_WAIT是主动关闭连接的一方在发出最后ACK后进入的状态作用是防止旧连接的延迟数据包干扰新连接。服务端自身主动断开连接时比如超时踢人、协议错误就会产生大量TIME_WAIT。很多人第一时间想到调net.ipv4.tcp_tw_reuse但要明白这个参数只对主动发起connect的一方有作用对服务端accept端的TIME_WAIT帮助很有限。我实际的做法是应用层尽量让客户端先关闭连接。如果服务端必须主动断先shutdown(SHUT_WR)然后等客户端FIN这样TIME_WAIT就落到了客户端那边。CLOSE_WAIT则代表对端已经发来FIN本端还没调用close。在libevent里出现大量CLOSE_WAIT几乎只有一个原因连接的事件回调里漏了bufferevent_free。检查代码里的EOF和ERROR分支确保每条断开的连接都被及时释放CLOSE_WAIT就会消失。5.2 边缘触发还是水平触发新手别直接上EV_ETlibevent默认是水平触发也就是只要fd还有数据没读完下一次epoll_wait会继续返回就绪状态。水平触发的好处是你不用担心漏数据即使一次read没读完后面总会再次触发。代价是如果你每次回调只读一两个字节事件循环会频繁空转造成CPU浪费。边缘触发EV_ET在状态变化时只触发一次性能更好但要求你的读回调必须尽量一次把数据读完否则剩余数据会等待下一个新数据包到达才触发造成延迟甚至饥饿。我在一个对低延迟很敏感的项目里用过ET模式配合EV_PERSIST和while循环读取确实把CPU省了下来但也付出了一些调试代价。如果你的应用协议比较规整数据包不大我建议先维持默认LT模式跑一阵子用perf top确认CPU确实主要花在了系统调用而不是事件回调上再考虑ET优化。不要一开始就上ET给自己加难度。5.3 每个连接的定时器别忘记释放也没必要过度优化libevent支持给每个连接挂超时事件非常方便。常见做法是创建会话结构时用event_new(base, -1, 0, cb, session)为目标连接创建一个定时器在读写回调里调用event_del后重新event_add来刷新超时时间。连接关闭时一定要记得event_free这个定时器。有一个常见的优化迷思是“几万个连接就有几万个定时器会不会很慢”实际上libevent内部用最小堆管理定时器插入和删除都是O(logN)几万个定时器完全不是问题。真正需要关注的是精度和粒度。如果业务只需要秒级超时不要把超时时间设置成几百毫秒级别的细粒度否则会让事件循环频繁醒来做无效检查。心跳超时我通常直接给60到120秒刷新超时时也只是更新堆里的key成本很低。5.4 event_base不是线程安全的跨线程唤醒用eventfd这个坑我见过很多次。有人为了让多个核一起处理事件直接在多个线程里同时对同一个event_base调用event_base_loop结果程序要么崩溃要么性能极差。libevent的event_base本身并未保证完全线程安全它在内部维护fd集合、事件队列、回调链表。就算你用evthread_use_pthreads()启用了全局锁也只是让接口调用的数据竞争减少事件回调之间的并发冲突仍然需要你自己处理。正确结构还是3.2说的多base多线程。但别忘记一件事一个线程accept到的连接要交给另一个线程的事件循环处理中间需要跨线程通知。最稳妥的做法是每个工作线程维护一个eventfd注册EV_READ事件。主线程往该eventfd写1字节工作线程的事件循环就会被唤醒。这里有一个经验被唤醒线程要做的事应该尽量少最好只是“去队列取任务”不要在通知线程里直接操作另一个线程的bufferevent。5.5 关闭连接的顺序先等缓冲区排空再freeBEV_EVENT_EOF表示收到对端的FIN也就是对端不再发送数据。但这不代表你这边不能继续发送。很多服务端在处理EOF时直接bufferevent_free如果此时发送缓冲区里还有排队的数据这些数据就会全部丢失。我的处理方法是在EOF事件里先bufferevent_disable(bev, EV_READ)然后检查输出缓冲长度。如果输出缓冲为空直接free如果不为空在可写回调或者事件回调里检测到输出缓冲变为0时再执行free。这样既保证不接收新数据也不丢弃已有数据。同理主动关闭连接前先shutdown(SHUT_WR)优雅地告诉对端“我已经发完了”等待对端FIN再进入释放流程。这套流程看代码有点琐碎但对长连接服务非常关键。5.6 事件回调里别做耗时操作要解耦就解耦彻底libevent的事件循环本质上是单线程的。回调函数如果执行一次需要50ms那这50ms内所有连接都会停下等待。最常见的反例是直接在回调里做数据库查询、磁盘写入、加锁等待或者和其他服务做同步网络调用。正确的做法是让事件循环只负责网络IO和轻量协议解析业务逻辑投递到独立线程池。投递时用有界队列防止业务处理不过来导致内存无限增长。当业务队列满了事件循环应该主动降低读取速度比如通过bufferevent_setwatermark调低读写水位或者暂时bufferevent_disable读事件。这个背压机制看起来简单但能让整个系统在极端流量下表现稳定而不是被拖垮。6. 选型建议高并发服务器并非只有libevent一条路6.1 libevent、libuv、Boost.Asio、裸epoll怎么选写完这套接入层后经常有人问我现在是不是应该直接用libuv或者Boost.Asio甚至上io_uring我的回答是选型取决于你的技术栈和项目约束。方案语言与抽象跨平台性能学习成本适合场景libeventC事件回调好高成熟稳定中低自研协议、网关接入层、嵌入式网络服务libuvC事件回调附带文件/TTY/DNS很好高中需要较多系统能力、跨平台Node风格Boost.AsioC异步对象协程支持好很好高高C大型工程偏爱类型安全与协程裸epollC完全手动仅Linux理论最高高极致性能、依赖极小、自己可控细节libevent最大的优点是轻和稳它不试图给你完整的网络框架只提供事件循环、bufferevent、连接监听这些基础件其余协议解析、状态机都由你自己搭。它也是很多老牌项目里实际使用的库稳定性经过大规模验证。如果你在C项目里想要更高层的抽象Boost.Asio的异步对象和协程风格会更顺手但构建依赖也更重。libuv在Windows和跨平台方面做得很好但如果你的服务就是跑在Linux上它相比libevent的优势并不明显。6.2 什么情况别用libevent我用血泪换来的认知libevent不是万能的下面几种场景我劝你别硬上。第一业务主要是CPU密集计算网络只是配角。此时瓶颈在CPU算力事件驱动并不能解决计算密集问题不如直接用简单阻塞模型配合业务线程池。第二你的团队完全不熟悉回调式编程而项目业务状态又极其复杂。过高的大状态机在回调里维护非常痛苦这时候C协程方案或者Boost.Asio的awaitable协程写起来会舒服很多。协程能把异步代码写得像同步代码对复杂业务逻辑的维护性提升非常明显。第三你要做的是超高密度、超低延时的极客级服务比如百万连接以上或者要求个位微秒延迟。libevent本身很优秀但每个事件回调的虚拟函数调用、evbuffer内部拷贝都有一定开销。新一代的io_uring模型可以显著减少系统调用次数但在内核版本要求、编程复杂度上代价也大。没有那个需求规模别自找麻烦。第四你的需求里需要大量的文件异步IO。libevent虽然也有对文件读写的支持但能力远不如libuv那么原生顺手。如果项目里网络和文件操作混得非常密切建议直接考虑libuv。6.3 我目前的最终组合和个人经验新项目里我的默认技术栈是libevent负责接入层的event_base和bufferevent业务侧用独立的线程池加有界队列做解耦协议解析层用状态机按会话结构实现跨线程唤醒用eventfd。这套组合兼顾了性能、稳定性和团队可维护性对我来说是最舒服的状态。如果今天让我从零开始做一个高并发的C接入服务我依然会选libevent入场但不会只依赖它。我会花一个晚上把5.2到5.6的坑全部提前堵上而不是上线之后再不断打补丁。最后分享一个小技巧优雅退出。生产环境里服务要被kill或重启时我不希望所有连接直接被系统断掉。做法是注册SIGINT和SIGTERM信号事件在信号回调里先停止accept新的连接再给所有在线连接一个宽限期把剩余数据发完最后调用event_base_loopbreak退出循环。这个能力libevent用evsignal_new实现起来很干净也是我认为它比裸epoll更完善的一个细节。

相关新闻

open-code-review:Diff驱动的CLI代码审查Agent

open-code-review:Diff驱动的CLI代码审查Agent

1. 这不是又一个“AI代码审查”玩具:open-code-review 的真实定位与生存逻辑 你肯定见过太多打着“AI Code Review”旗号的工具——网页端点几下、上传个文件、等30秒吐出几条泛泛而谈的“建议”,比如“变量名可读性待提升”“考虑添加类型注解”。它们…

2026/10/19 13:35:41 阅读更多 →
TCP到HTTP的网关性能优化:高并发下的调优实战与踩坑复盘

TCP到HTTP的网关性能优化:高并发下的调优实战与踩坑复盘

最近接手了一个内部网关服务的性能优化,项目代号里带了一串特殊字符,不重要,核心任务只有一个:把网络IO层的性能压榨出来。高峰期连接超时、CPU 飙高、千兆网卡死活跑不满,这些问题相信不少人都遇到过。这篇就当一次完…

2026/10/19 13:35:41 阅读更多 →
从聊天机器人到AI员工团:多Agent协同实战指南

从聊天机器人到AI员工团:多Agent协同实战指南

先说一个我最近常被问到的问题:聊天机器人做得挺顺了,但老板开始问"能不能让AI把我这摊活儿也干了",这时候该怎么办?如果你也走到了这一步,那说明我们已经越过了"问答式AI"的阶段,进入…

2026/10/19 13:35:41 阅读更多 →

最新新闻

软考软件设计师:程序设计语言基础与编译原理考点全梳理

软考软件设计师:程序设计语言基础与编译原理考点全梳理

软考软件设计师上午题一共75道选择题,涉及的知识面铺得非常广。很多人复习到“程序设计语言基础知识”这一章时,都会有同样的感觉:内容不难,但概念太细,编译原理相关的几个阶段总是看完就混,文法那四种类型…

2026/10/19 14:15:32 阅读更多 →
Node.js安装与npm环境配置:从版本选择到镜像加速全流程

Node.js安装与npm环境配置:从版本选择到镜像加速全流程

第一次接触Node.js的人,十个里有七个会倒在“装环境”这一步:官网上一堆版本号不知道选哪个,安装包下载完了一路点Next,最后在终端敲node -v却提示“不是内部或外部命令”。这篇文章就是专门解决这个问题的。我会从下载前的版本选…

2026/10/19 14:15:32 阅读更多 →
iSCSI网络存储详解:从块存储原理到生产环境调优与排障

iSCSI网络存储详解:从块存储原理到生产环境调优与排障

iSCSI这个词,只要做过服务器运维或者数据中心相关工作,应该都不陌生。它本质上是把SCSI存储协议封装到TCP/IP网络里传输,让远端主机像插着一块本地硬盘一样使用网络上的存储设备。说白了,就是用网线把存储空间“拉”到服务器上&am…

2026/10/19 14:15:32 阅读更多 →
4D-Animal:用密集线索与工具链从普通视频重建可动画3D动物模型

4D-Animal:用密集线索与工具链从普通视频重建可动画3D动物模型

视频里一只狗摇着尾巴转了个圈,随后小跑两步,头低下去嗅地面。这时候你手里只有这段普通素材,想要的是一个能在动画软件里旋转、换动作、甚至重新绑定到别的骨骼上的可动画3D狗。放在以前,绝大多数团队的第一反应是:先…

2026/10/19 14:15:32 阅读更多 →
商用车VCU硬件设计:从MPC5744原理图到量产避坑指南

商用车VCU硬件设计:从MPC5744原理图到量产避坑指南

简介:面向新能源整车控制器与嵌入式硬件开发者的VCU硬件设计资料,围绕NXP的MPC5744主控芯片,详解商用车量产整车控制器的硬件原理图与PCB设计,适合电控工程师、嵌入式开发者及车辆工程专业学生参考。压缩包共13个文件,…

2026/10/19 14:15:32 阅读更多 →
IEC 62619:2022工业锂电池安全标准详解:测试项与BMS功能安全

IEC 62619:2022工业锂电池安全标准详解:测试项与BMS功能安全

简介:IEC 62619:2022(第二版,2022年5月)是国际电工委员会发布的工业用二次锂电池与蓄电池安全要求标准,主要面向电池研发、制造、测试与工业设备集成等工程人员,为产品安全设计、性能验证和合规评估提供统一…

2026/10/19 14:14:30 阅读更多 →

日新闻

频率跟踪型PWM电源设计:臭氧发生器谐振点漂移与软开关实现

频率跟踪型PWM电源设计:臭氧发生器谐振点漂移与软开关实现

简介:本资源为电源技术方向的学术研究文档,聚焦介质阻挡放电型臭氧发生器电源的频率跟踪型PWM控制策略,面向电力电子、高压电源及臭氧应用领域的科研人员与工程技术人员。针对负载呈容性、等效电容随外加电压升高而增大、谐振频率随之漂移的难…

2026/10/19 0:00:47 阅读更多 →
Vibe Coding 遇上 TaoToken:把开发者思维圣殿的灵感流接入统一 Key 通道

Vibe Coding 遇上 TaoToken:把开发者思维圣殿的灵感流接入统一 Key 通道

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/19 0:00:47 阅读更多 →
HundredCV-Chat话题建模实战教程:利用4,871个内置话题标签构建主题分析系统

HundredCV-Chat话题建模实战教程:利用4,871个内置话题标签构建主题分析系统

【免费下载链接】HundredCV-Chat 项目地址: https://ai.gitcode.com/hf_mirrors/Jax-dan/HundredCV-Chat 点击查看 免费下载 HundredCV-Chat 是一个由 100 位青年简历数据开发而来的中文多轮对话数据集,共包含 24,750 组日常闲聊对话和 4,871 个话题标签…

2026/10/19 0:00:47 阅读更多 →

周新闻

基于GMM的语音识别系统原理与MATLAB实现详解

基于GMM的语音识别系统原理与MATLAB实现详解

说到基于高斯混合模型(GMM)的语音识别系统,很多人第一反应就是一堆数学公式,觉得门槛很高。但如果你真的在MATLAB里把它落地跑通,会发现核心其实就是三件事:把声音变成特征向量,用GMM给每个类别…

2026/10/19 10:02:04 阅读更多 →
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理这四个字听起来挺唬人,但落到具体操作上,无非就是让计算机按你的想法“摆弄”像素。我在一个模拟项目X里用了一套特别经典的基础处理流程:对图像做反白、改调色板、彩色转灰度,最后把像素数据导出成txt文件。单看每一…

2026/10/18 0:00:55 阅读更多 →
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

机器人大脑技术盘点:具身智能的感知、决策与数据闭环

如果你的信息流最近也天天刷到“机器人大脑”这个词,你会发现一个很有意思的现象:每个人说的“大脑”根本不是同一个东西。有人指的是大语言模型,有人指的是运动控制系统,还有人指的是能理解物理世界的端到端模型。在2026年这个时…

2026/10/19 1:30:52 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/19 2:54:50 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/19 9:10:22 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/18 12:16:16 阅读更多 →