ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

阻塞非阻塞与同步异步的本质区别和工程实践

阻塞非阻塞与同步异步的本质区别和工程实践 1. 这不是概念辨析题而是系统设计的底层罗盘你翻过无数篇讲“阻塞/非阻塞/同步/异步”的文章最后发现——越看越糊涂。不是因为概念本身有多难而是绝大多数讲解者把这四个词当成孤立的名词在解释像背英语单词一样告诉你“blocking means waiting”“asynchronous means callback”。结果呢写代码时依然分不清read()是阻塞还是非阻塞async/await到底解决了哪一层的等待pthread_mutex_lock()和epoll_wait()的本质差异在哪。我干了十多年底层开发、高并发服务、嵌入式通信和GUI框架适配踩过的坑足够填满三个Linux内核缓冲区。今天不讲定义只讲真实世界里这四个词怎么打架、怎么协作、怎么决定你程序的生死线。核心关键词“阻塞”“非阻塞”“同步”“异步”不是并列关系而是两组正交维度调用方是否被挂起阻塞 vs 非阻塞任务执行与结果返回是否分离同步 vs 异步。这个二维坐标系才是你设计任何I/O、网络、UI、数据库交互的底层罗盘。比如你问“tkinter能否在没有mainloop主线中打开一个非阻塞的窗口”问题本身已经暴露了混淆——mainloop是事件循环机制而“非阻塞窗口”根本不存在真正能做的是让窗口创建不阻塞当前线程或让窗口事件处理不阻塞其他逻辑。再比如“uart阻塞和非阻塞”本质是串口驱动层对read()系统调用的实现策略和上层应用是否同步等待无关。还有“异步复位同步释放”这是数字电路里的时序约束和软件里的异步编程毫无关系只是碰巧用了同一个词。这些热词堆砌背后是大量开发者在不同抽象层级上混用同一套术语导致沟通失效、设计错位、调试崩溃。本文所有内容都来自我在金融交易系统做毫秒级延迟优化、在工业PLC网关里调试UART丢包、在车载HMI上解决Tkinter界面卡死、在数据库中间件里重构同步复制链路的真实战场。每一个结论都对应着一次线上告警、一次客户投诉、一次连续三天的core dump分析。下面我们直接撕开操作系统、硬件驱动、编程语言、应用框架这四层皮看清楚这四个词到底长什么样。2. 四维解构从CPU寄存器到Python装饰器的全栈真相2.1 操作系统内核视角系统调用才是真正的分水岭所有关于阻塞/非阻塞/同步/异步的混乱根源在于没看清系统调用syscall这个关键枢纽。用户空间程序无法直接操作硬件必须通过内核提供的接口。而内核对每个系统调用的实现方式直接决定了上层行为的归类。以最典型的read()为例阻塞式read()当内核发现文件描述符fd当前无数据可读比如socket接收缓冲区为空、串口RX FIFO为空它会把当前进程的task_struct状态设为TASK_INTERRUPTIBLE移出运行队列放入该fd对应的等待队列wait queue然后触发调度器切换到其他进程。此时CPU完全不为你干活你就在那里“睡着”。直到有数据到达中断触发、超时发生或收到信号内核才把你唤醒重新进入运行队列。这个过程就是阻塞——调用方被强制挂起资源CPU时间片被剥夺。非阻塞式read()内核检查fd状态若无可读数据立即返回错误码EAGAIN或EWOULDBLOCK绝不挂起进程。调用方拿到这个错误必须自己决定下一步是立刻重试轮询、还是去干别的事、或是注册一个事件通知如epoll。这就是非阻塞——调用方永远不睡觉但需要自己承担“查岗”的成本。提示O_NONBLOCK标志是在open()或fcntl()时设置的它改变的是内核对该fd后续所有I/O系统调用的行为不是某个函数的属性。很多新手以为read(fd, buf, len)加个参数就能变非阻塞这是根本性误解。而同步 vs 异步在内核层面体现在结果交付方式上同步I/Oread()返回时数据已从内核缓冲区拷贝到用户提供的buf中。整个过程等待数据拷贝由一次系统调用完成调用方拿到的就是最终结果。这是99%的常规I/O。异步I/OAIO调用io_submit()提交一个读请求后内核立即返回不等数据就绪更不拷贝数据。数据真正准备好并拷贝完成后内核通过信号SIGIO或回调函数io_getevents通知用户。调用方和结果获取完全解耦。Linux的libaio和 POSIX AIOaio_read走这条路但实际使用率极低因为复杂且性能未必更好。所以read()本身是同步I/O但它可以是阻塞的默认也可以是非阻塞的O_NONBLOCK。io_submit()是异步I/O它必然是非阻塞的否则就矛盾了。这四个词的组合本质是内核提供给用户的两种开关一个控制“是否挂起”一个控制“是否解耦”。2.2 硬件与驱动层UART、网卡、磁盘的物理真相概念脱离硬件就是空中楼阁。我们看几个热词里的真实场景UART阻塞和非阻塞串口控制器如16550 UART有个8字节FIFO。驱动层read()时阻塞模式驱动检查FIFO空则调用wait_event_interruptible()让进程睡眠等RX中断到来硬件把字节写入FIFO并触发中断再唤醒。非阻塞模式驱动检查FIFO空则直接返回-EAGAIN。上层应用要么忙等耗电要么用select()/poll()监听该fd。 关键点阻塞/非阻塞是软件驱动的策略UART硬件本身没有“阻塞”能力它只负责收发字节和发中断。硬件同步EtherCAT DC时钟这是完全不同的维度。“同步”在这里指分布式时钟Distributed Clocks机制主站通过精确测量各从站时钟偏移下发校准指令使所有从站本地时钟相位差小于1微秒。这和软件I/O的“同步”毫无关系它解决的是物理时间一致性问题用于运动控制的硬实时场景。混淆二者会导致你在PLC程序里错误地用pthread_cond_signal()去“同步”伺服轴结果当然是灾难性的。同步整流 vs 异步整流Buck电路又是另一个世界。同步整流用MOSFET代替二极管由控制器精确控制导通/关断时机降低导通损耗异步整流用二极管靠其单向导电性自然续流。这里的“同步/异步”描述的是功率器件的开关时序是否受控于主控制器和软件编程范式无关。把它和async/await放一起讨论就像拿菜刀和CPU指令集比“哪个更快”。2.3 编程语言与运行时Python、C#、JS的封装迷雾语言Runtime在内核syscall之上加了一层又一层抽象目的本是简化结果常是混淆Pythonasync/await这是协程coroutine机制本质是用户态的轻量级线程调度。await asyncio.sleep(1)并不调用nanosleep()系统调用而是把当前协程挂起交出控制权给event loop让其他协程运行。event loop底层用epoll/kqueue/IOCP监听I/O事件事件就绪后恢复对应协程。所以async/await解决的是单线程内高并发I/O的效率问题它本身不改变系统调用的阻塞/非阻塞属性——你await的那个read()底层依然是阻塞或非阻塞的。asyncio.open_connection()创建的stream其read()方法内部会自动处理EAGAIN并挂起协程这是库的封装不是魔法。C#async/await与Task.Run()await Task.Run(() HeavyComputation())是把CPU密集型任务扔到线程池执行这和I/O异步无关。真正的I/O异步如FileStream.ReadAsync()底层调用Windows的IOCP是真正的异步I/O。混淆这两者会导致你在Web API里用Task.Run()去读大文件线程池被耗尽整个服务雪崩。JavaScript Promise“Promise是异步的”这句话大错特错。Promise.resolve(42)是同步执行的then()回调在当前宏任务结束后、下一个宏任务开始前的微任务队列里执行。fetch()才是真正的异步I/O它启动网络请求后立即返回Promise结果通过回调交付。JS的“异步”主要指事件循环模型下的非阻塞执行和操作系统级的异步I/OAIO不是一回事。2.4 应用框架与中间件数据库同步、消息队列的工程实相到了业务层术语进一步泛化但底层逻辑不变数据库同步软件MySQL主从复制主库的binlog写入是同步的事务提交前必须刷盘但从库的SQL线程回放binlog是单线程串行执行这是性能瓶颈。所谓“异步复制”是指主库不等待从库确认就返回成功复制延迟是常态。而“半同步复制”要求至少一个从库落盘才返回这是用延迟换一致性。这里的“同步/异步”描述的是主从之间的一致性保证级别不是I/O模式。阻塞队列Java BlockingQueueput()方法在队列满时会阻塞当前线程直到有空间take()在空时阻塞直到有元素。这是线程间协调的同步原语利用ReentrantLock和Condition实现和I/O阻塞无关。它的“阻塞”是线程调度层面的目的是避免忙等浪费CPU。线程池的阻塞队列选择ArrayBlockingQueue有界 vsLinkedBlockingQueue默认无界。选错会导致OOM无界队列积压任务或拒绝服务有界队列满后拒绝新任务。这不是I/O问题而是背压backpressure控制策略。3. 实操拆解五个高频场景的落地代码与避坑指南3.1 场景一Linux下UART非阻塞读取嵌入式/工控目标从/dev/ttyS0读取传感器数据绝不阻塞主线程支持超时和错误处理。#include stdio.h #include stdlib.h #include string.h #include unistd.h #include fcntl.h #include errno.h #include sys/time.h #include termios.h int setup_uart_nonblock(const char *dev_path) { int fd open(dev_path, O_RDWR | O_NOCTTY | O_NDELAY); if (fd 0) { perror(open uart); return -1; } // 关键清除O_NDELAY设置O_NONBLOCK更标准 int flags fcntl(fd, F_GETFL); fcntl(fd, F_SETFL, flags | O_NONBLOCK); struct termios tty; memset(tty, 0, sizeof(tty)); if (tcgetattr(fd, tty) ! 0) { perror(tcgetattr); close(fd); return -1; } cfsetospeed(tty, B115200); cfsetispeed(tty, B115200); tty.c_cflag ~PARENB; // 无校验 tty.c_cflag ~CSTOPB; // 1停止位 tty.c_cflag ~CSIZE; tty.c_cflag | CS8; // 8数据位 tty.c_cflag ~CRTSCTS;// 无硬件流控 tty.c_cflag | CREAD | CLOCAL; // 允许接收忽略modem控制信号 tty.c_iflag ~(IXON | IXOFF | IXANY); // 无软件流控 tty.c_lflag ~(ICANON | ECHO | ECHOE | ISIG); // 原始模式 tty.c_oflag ~OPOST; // 关键设置最小读取字符数和超时 tty.c_cc[VMIN] 0; // 非阻塞不等待最小字符数 tty.c_cc[VTIME] 0; // 不等待超时VTIME0且VMIN0即纯非阻塞 if (tcsetattr(fd, TCSANOW, tty) ! 0) { perror(tcsetattr); close(fd); return -1; } return fd; } // 主循环非阻塞读取带超时检测 void uart_read_loop(int fd) { char buf[256]; ssize_t n; struct timeval start, now; long timeout_ms 1000; // 1秒超时 gettimeofday(start, NULL); while (1) { n read(fd, buf, sizeof(buf)-1); if (n 0) { buf[n] \0; printf(Received: %s, buf); gettimeofday(start, NULL); // 重置超时计时器 } else if (n 0) { // EOF串口可能断开 printf(UART EOF\n); break; } else { if (errno EAGAIN || errno EWOULDBLOCK) { // 无数据检查超时 gettimeofday(now, NULL); long elapsed (now.tv_sec - start.tv_sec) * 1000 (now.tv_usec - start.tv_usec) / 1000; if (elapsed timeout_ms) { printf(Timeout waiting for data\n); break; } usleep(10000); // 10ms后重试避免CPU空转 } else { perror(read error); break; } } } }避坑指南O_NDELAY已废弃务必用O_NONBLOCK。VMIN0且VTIME0是纯非阻塞模式VMIN1且VTIME0是“有数据就读没数据立刻返回”也属非阻塞。usleep(10000)是必要的否则while(1)会100%占用一个CPU核。工业现场常用select()监听fd比忙等优雅得多。串口线干扰可能导致read()返回部分数据如只读到帧头需自行实现帧解析协议如STX/ETX CRC。3.2 场景二Python asyncio PostgreSQL 异步查询Web服务目标在FastAPI中并发查询多张表避免GIL和I/O阻塞。# requirements.txt # asyncpg0.29.0 # fastapi0.111.0 # uvicorn0.29.0 import asyncio import asyncpg from fastapi import FastAPI from typing import List, Dict app FastAPI() # 全局连接池避免每次请求新建连接 pool None app.on_event(startup) async def startup(): global pool # 关键连接池配置max_size控制并发连接数 pool await asyncpg.create_pool( hostlocalhost, port5432, usermyuser, passwordmypass, databasemydb, min_size10, # 初始化最小连接数 max_size100, # 最大连接数防爆 max_inactive_connection_lifetime300, # 5分钟空闲连接回收 command_timeout60, # 查询超时60秒 ) app.on_event(shutdown) async def shutdown(): if pool: await pool.close() app.get(/data) async def get_data(): # 并发执行三个查询await会挂起当前协程让event loop调度其他任务 results await asyncio.gather( fetch_users(), fetch_orders(), fetch_products() ) return {users: results[0], orders: results[1], products: results[2]} async def fetch_users() - List[Dict]: async with pool.acquire() as conn: # 从连接池获取连接非阻塞 # asyncpg的execute/query都是真正的异步I/O rows await conn.fetch(SELECT id, name FROM users LIMIT 10) return [dict(row) for row in rows] async def fetch_orders() - List[Dict]: async with pool.acquire() as conn: rows await conn.fetch(SELECT order_id, total FROM orders WHERE statuspaid LIMIT 10) return [dict(row) for row in rows] async def fetch_products() - List[Dict]: async with pool.acquire() as conn: rows await conn.fetch(SELECT sku, price FROM products WHERE stock 0 LIMIT 10) return [dict(row) for row in rows]避坑指南asyncpg是唯一生产级的PostgreSQL异步驱动aiopg基于psycopg2本质是线程池包装不是真异步。asyncio.gather()并发执行比串行await快3倍以上。但注意并发数受连接池max_size限制超过会排队等待连接。pool.acquire()是异步的它可能因连接池满而挂起协程这是预期行为不是bug。绝对不要在async函数里调用time.sleep()或requests.get()它们是阻塞的会拖垮整个event loop。要用asyncio.sleep()和httpx.AsyncClient。3.3 场景三Tkinter GUI与后台任务解耦桌面应用目标点击按钮启动耗时计算界面不卡死能显示进度。import tkinter as tk from tkinter import ttk, messagebox import threading import time import queue class App: def __init__(self, root): self.root root self.root.title(Tkinter Non-blocking Demo) self.root.geometry(400x300) # UI组件 self.start_btn ttk.Button(root, textStart Calculation, commandself.start_calc) self.start_btn.pack(pady10) self.progress ttk.Progressbar(root, modedeterminate, maximum100) self.progress.pack(pady10, fillx, padx20) self.status_label ttk.Label(root, textReady) self.status_label.pack(pady10) # 线程安全的队列用于线程间通信 self.result_queue queue.Queue() self.is_running False def start_calc(self): if self.is_running: messagebox.showwarning(Warning, Calculation is already running!) return self.is_running True self.start_btn.config(statedisabled) self.status_label.config(textCalculating...) # 启动后台线程不阻塞主线程 calc_thread threading.Thread(targetself._background_calculation, daemonTrue) calc_thread.start() # 启动检查队列的定时器Tkinter的after是安全的 self.root.after(100, self._check_result_queue) def _background_calculation(self): 后台线程执行耗时任务 try: # 模拟耗时计算 for i in range(101): time.sleep(0.05) # 每步50ms # 更新进度通过队列通知主线程 self.result_queue.put((progress, i)) # 计算完成 result fDone! Sum {sum(range(10000))} self.result_queue.put((result, result)) except Exception as e: self.result_queue.put((error, str(e))) finally: self.result_queue.put((done, None)) def _check_result_queue(self): 主线程定期检查队列 try: while True: msg_type, data self.result_queue.get_nowait() if msg_type progress: self.progress[value] data elif msg_type result: self.status_label.config(textfResult: {data}) elif msg_type error: messagebox.showerror(Error, data) self.status_label.config(textError occurred) elif msg_type done: self.is_running False self.start_btn.config(statenormal) self.status_label.config(textCalculation finished) return except queue.Empty: pass # 继续检查 if self.is_running: self.root.after(100, self._check_result_queue) if __name__ __main__: root tk.Tk() app App(root) root.mainloop()避坑指南tkinter的mainloop()是单线程事件循环任何耗时操作都必须放到独立线程或进程否则界面冻结。asyncio与tkinter天然不兼容强行混合会导致未定义行为。threading.Thread的daemonTrue表示主线程退出时该线程自动结束避免程序无法退出。queue.Queue是线程安全的get_nowait()避免阻塞主线程。root.after(100, ...)是Tkinter推荐的跨线程UI更新方式比root.update()更安全。不要尝试用asyncio驱动tkinter社区有tkinter-async等实验库但稳定性远不如线程方案。3.4 场景四C Winsock2 非阻塞 recvWindows客户端目标Windows下Socket接收数据不阻塞主线程支持多连接。#include winsock2.h #include ws2tcpip.h #include iostream #include vector #include unordered_map #include chrono #include thread #pragma comment(lib, ws2_32.lib) class NonBlockingClient { private: SOCKET sock_; std::unordered_mapSOCKET, std::vectorchar recv_buffers_; public: NonBlockingClient() : sock_(INVALID_SOCKET) {} bool connect(const char* ip, int port) { WSADATA wsaData; if (WSAStartup(MAKEWORD(2, 2), wsaData) ! 0) { std::cerr WSAStartup failed\n; return false; } sock_ socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); if (sock_ INVALID_SOCKET) { std::cerr socket failed\n; return false; } // 关键设置非阻塞模式 u_long mode 1; // 1 non-blocking if (ioctlsocket(sock_, FIONBIO, mode) SOCKET_ERROR) { std::cerr ioctlsocket failed\n; closesocket(sock_); return false; } sockaddr_in server_addr{}; server_addr.sin_family AF_INET; server_addr.sin_port htons(port); inet_pton(AF_INET, ip, server_addr.sin_addr); // connect()在非阻塞模式下立即返回成功或失败WSAEWOULDBLOCK int result connect(sock_, (sockaddr*)server_addr, sizeof(server_addr)); if (result SOCKET_ERROR) { int err WSAGetLastError(); if (err WSAEWOULDBLOCK) { // 连接进行中需用select检查 std::cout Connecting...\n; return true; // 继续轮询 } else { std::cerr connect failed: err \n; closesocket(sock_); return false; } } std::cout Connected!\n; return true; } // 主循环轮询recv void run() { fd_set read_fds; timeval timeout{1, 0}; // 1秒超时 while (true) { FD_ZERO(read_fds); FD_SET(sock_, read_fds); int activity select(0, read_fds, nullptr, nullptr, timeout); if (activity 0) { std::cerr select error\n; break; } else if (activity 0) { // 超时继续 continue; } if (FD_ISSET(sock_, read_fds)) { char buffer[1024]; int bytes_received recv(sock_, buffer, sizeof(buffer)-1, 0); if (bytes_received 0) { buffer[bytes_received] \0; std::cout Received: buffer \n; } else if (bytes_received 0) { std::cout Connection closed by server\n; break; } else { int err WSAGetLastError(); if (err WSAEWOULDBLOCK || err WSAEAGAIN) { // 无数据继续轮询 continue; } else { std::cerr recv error: err \n; break; } } } } closesocket(sock_); WSACleanup(); } };避坑指南ioctlsocket(sock_, FIONBIO, mode)是Windows设置非阻塞的唯一正确方式fcntl()在Windows不可用。connect()在非阻塞模式下会立即返回WSAEWOULDBLOCK表示连接正在建立必须用select()检查可写性write_fds来判断连接是否成功。select()的第一个参数在Windows上被忽略传0即可timeval设置超时避免无限等待。recv()返回0表示对方关闭连接FIN返回-1且WSAGetLastError()为WSAECONNRESET表示连接被重置。3.5 场景五数据库同步工具实战MySQL主从复制搭建目标将远程MySQL库的orders表同步到本地支持增量。# 步骤1配置主库remote server # 编辑 /etc/mysql/mysql.conf.d/mysqld.cnf [mysqld] server-id 1 log-bin mysql-bin binlog-format ROW # 推荐记录行变更更安全 expire_logs_days 7 max_binlog_size 100M # 重启主库 sudo systemctl restart mysql # 登录主库创建复制用户 mysql -u root -p CREATE USER repl% IDENTIFIED BY StrongPass123!; GRANT REPLICATION SLAVE ON *.* TO repl%; FLUSH PRIVILEGES; # 查看主库状态记下File和Position SHOW MASTER STATUS; # ------------------------------------------------------------ # | File | Position | Binlog_Do_DB | Binlog_Ignore_DB | # ------------------------------------------------------------ # | mysql-bin.000001 | 154 | | | # ------------------------------------------------------------ # 步骤2配置从库local server # 编辑 /etc/mysql/mysql.conf.d/mysqld.cnf [mysqld] server-id 2 relay-log mysql-relay-bin read_only ON # 防止误写 # 重启从库 sudo systemctl restart mysql # 步骤3在从库执行CHANGE MASTER TO mysql -u root -p CHANGE MASTER TO MASTER_HOSTremote_ip, MASTER_USERrepl, MASTER_PASSWORDStrongPass123!, MASTER_LOG_FILEmysql-bin.000001, MASTER_LOG_POS154; # 启动复制 START SLAVE; # 检查状态 SHOW SLAVE STATUS\G # 关键字段 # Slave_IO_Running: Yes # IO线程正常从主库拉取binlog # Slave_SQL_Running: Yes # SQL线程正常回放binlog # Seconds_Behind_Master: 0 # 延迟为0同步正常避坑指南server-id在整个复制集群中必须唯一主从不能相同。binlog-formatROW是底线STATEMENT格式在函数、时间戳等场景下会导致主从不一致。read_onlyON只对普通用户生效root用户仍可写生产环境应严格限制root权限。Seconds_Behind_Master为0不代表绝对实时网络抖动、大事务都会造成瞬时延迟。若需只同步特定表可在从库配置replicate-do-tabledb_name.table_name但会增加维护复杂度。4. 常见问题速查表与独家排错心法4.1 高频问题诊断矩阵现象可能原因排查命令/方法解决方案Python asyncio程序CPU 100%asyncio.sleep(0)或await asyncio.sleep(0)被滥用或while True:内无awaitstrace -p pid -e traceepoll_wait查看是否在epoll上空转检查所有循环确保有真正的I/Oawait或asyncio.sleep()用asyncio.to_thread()包装CPU密集型任务Tkinter界面卡死但后台线程在跑后台线程直接调用root.update()或修改UI组件print(threading.current_thread().name)确认线程名严格使用queue.Queueroot.after()通信禁止跨线程UI操作UART非阻塞读取总是返回EAGAIN串口线未接好、波特率不匹配、硬件流控开启stty -F /dev/ttyS0查看当前设置用逻辑分析仪抓波形用stty命令确认crtscts关闭-icanon -echo开启用万用表测TX/RX电压MySQL主从延迟飙升主库有大事务如ALTER TABLE、从库I/O或SQL线程卡住、网络带宽不足SHOW PROCESSLIST查看Slave_SQL_Running线程状态iostat -x 1看磁盘IO拆分大事务升级从库硬件启用并行复制slave_parallel_workers4C Winsock recv()返回0但连接未断对方发送了FIN包但你的程序没处理netstat -ano | findstr port查看连接状态recv()返回0即TCP连接关闭应清理socket并退出循环4.2 我踩过的三个血泪坑坑一在asyncio里用threading.Lock现象多个协程并发访问一个共享字典加了threading.Lock()结果还是数据错乱。 原因threading.Lock()是线程锁而asyncio协程都在同一个线程里切换。await会让出控制权但锁不会释放导致其他协程永远拿不到锁形成死锁。 解决方案用asyncio.Lock()它是协程安全的lock asyncio.Lock() async with lock: shared_dict[key] value坑二认为“异步快”盲目替换所有I/O现象把一个每秒处理10次请求的Flask应用全部改成async/awaitQPS反而从10降到3。 原因Flask默认不支持async view强行用async def会导致event loop被阻塞且该应用90%时间花在CPU计算图像处理不是I/O等待。 解决方案识别真正的I/O瓶颈数据库、HTTP调用只对这些部分异步化CPU密集型任务用ProcessPoolExecutor或直接换用支持async的框架FastAPI、Starlette。坑三UART非阻塞VTIME1的诡异行为现象设置VTIME11分秒超时read()却总在1秒后返回无论是否有数据。 原因VTIME单位是十分之一秒VTIME1是0.1秒不是1秒且VTIME只在VMIN0时生效表示“等待第一个字节的最长时间”。 解决方案查man termiosVTIME的单位是0.1秒VTIME10才是1秒若要精确超时用select()更可靠。4.3 参数选择黄金法则epoll的EPOLLINvsEPOLLETEPOLLIN是水平触发LT只要fd有数据就持续通知EPOLLET是边缘触发ET只在状态变化时通知一次。ET性能更高但要求你必须一次性读完所有数据循环read()直到EAGAIN否则会漏事件。生产环境推荐ET但新手从LT起步。线程池队列大小公式queue_size (avg_task_time_ms / 1000) * target_qps * safety_factor。例如平均任务
返回列表