"阻塞"和"非阻塞"主要是在说:当一个操作暂时无法完成时,调用者是停在那里等,还是立刻返回。
它经常出现在:
read()
write()
recv()
send()
accept()
connect()
尤其是网络编程里。
一、阻塞是什么意思
阻塞就是:函数暂时做不了,就一直等,直到能做为止。
比如:
char buf[1024];
recv(sockfd, buf, sizeof(buf), 0);
如果 sockfd 是阻塞 socket,并且现在没有数据可读,那么 recv 会卡住。
程序会停在这里:
recv(sockfd, buf, sizeof(buf), 0);
// 没数据就不往下走
直到发生下面某种情况:
对端发来了数据
对端关闭连接
出错
被信号打断
设置了超时并超时
所以阻塞 I/O 的感觉是:
你要读数据?
现在没数据。
那你在这里等着。
二、 非阻塞是什么意思
非阻塞就是:函数暂时做不了,也不会等,而是马上返回。
同样是:
recv(sockfd, buf, sizeof(buf), 0);
如果 sockfd 是非阻塞 socket,并且现在没有数据可读,那么 recv 不会卡住,而是返回:
-1
并设置:
errno == EAGAIN || errno == EWOULDBLOCK
意思是:
现在没数据,你之后再试。
所以非阻塞 I/O 的感觉是:
你要读数据?
现在没数据。
我不等,直接告诉你暂时不能读。
三、 一个直观比喻
阻塞:
你去奶茶店买奶茶。
店员说还没做好。
你站在柜台前一直等,什么都不干。
等做好后,拿到奶茶再走。
非阻塞:
你去奶茶店问好了没。
店员说还没好。
你马上离开,去做别的事。
过一会儿再来问。
在程序里,"站在那里等"就是线程被阻塞。
四、阻塞 read/recv 示例
char buf[1024];
ssize_t n = recv(fd, buf, sizeof(buf), 0);
std::cout << "recv returned\n";
如果 fd 是阻塞的,并且没有数据,那么:
std::cout << "recv returned\n";
这一行不会执行,直到 recv 返回。
也就是说,整个线程卡在 recv 上。
五、 非阻塞 recv 示例
先把 socket 设置成非阻塞:
#include <fcntl.h>
#include <unistd.h>
void set_nonblocking(int fd) {
int flags = fcntl(fd, F_GETFL, 0);
fcntl(fd, F_SETFL, flags | O_NONBLOCK);
}
然后:
char buf[1024];
ssize_t n = recv(fd, buf, sizeof(buf), 0);
if (n > 0) {
// 读到了 n 字节
} else if (n == 0) {
// 对端关闭连接
} else {
if (errno == EAGAIN || errno == EWOULDBLOCK) {
// 现在没数据,不是错误,之后再读
} else {
// 真正的错误
}
}
非阻塞 I/O 的核心就是要处理:
EAGAIN
EWOULDBLOCK
它们表示"现在做不了,但不是致命错误"。
六、accept 的阻塞和非阻塞
accept 用来接受新连接。
阻塞模式下:
int client_fd = accept(listen_fd, nullptr, nullptr);
如果没有客户端连接进来,程序就卡住。
非阻塞模式下:
int client_fd = accept(listen_fd, nullptr, nullptr);
如果暂时没有新连接,返回 -1,并且:
errno == EAGAIN || errno == EWOULDBLOCK
所以非阻塞服务器里经常这样写:
while (true) {
int client_fd = accept(listen_fd, nullptr, nullptr);
if (client_fd >= 0) {
set_nonblocking(client_fd);
// 加入 epoll
} else {
if (errno == EAGAIN || errno == EWOULDBLOCK) {
break;
}
// 其他错误
break;
}
}
这里的 while 很常见,尤其是 epoll 的边缘触发模式下。
七、 send/write 的阻塞和非阻塞
很多人以为 send 一定能立刻把数据发出去,其实不是。
阻塞 socket:
send(fd, data, len, 0);
如果内核发送缓冲区满了,send 会卡住,直到缓冲区有空间。
非阻塞 socket:
ssize_t n = send(fd, data, len, 0);
如果发送缓冲区满了,它返回:
-1
并设置:
errno == EAGAIN || errno == EWOULDBLOCK
还有一个重要点:send 可能只发送一部分。
比如你想发 10000 字节:
ssize_t n = send(fd, data, 10000, 0);
返回值可能是:
3000
意思是只发出去了 3000 字节,剩下 7000 字节要你之后继续发。
所以非阻塞网络程序里一般要有发送缓冲区:
struct Connection {
int fd;
std::string write_buffer;
};
没发完的数据放进 write_buffer,等下次 EPOLLOUT 再发。
八、connect 的阻塞和非阻塞
connect 也有区别。
阻塞模式:
connect(fd, addr, len);
会一直等到连接成功或失败。
非阻塞模式下:
int ret = connect(fd, addr, len);
如果连接还在进行中,通常返回:
-1
并且:
errno == EINPROGRESS
意思是:
连接正在建立,还没完成。
这时你可以用 epoll 监听这个 fd 的可写事件:
EPOLLOUT
等它可写时,再用 getsockopt 检查是否连接成功。
九、阻塞的问题
阻塞本身不是坏事。
简单程序里,阻塞很好用:
while (true) {
int client = accept(listen_fd, nullptr, nullptr);
recv(client, buf, sizeof(buf), 0);
send(client, response, response_len, 0);
}
代码直观。
但高并发服务器里,阻塞会带来问题。
假设一个线程处理一个连接:
10000 个连接 = 10000 个线程
这会有:
线程创建成本高
上下文切换成本高
栈内存占用大
调度开销大
慢连接会占住线程
如果一个客户端一直不发数据,线程就一直卡在 recv 上。
十、非阻塞的问题
非阻塞也不是万能的。
如果你这么写:
while (true) {
ssize_t n = recv(fd, buf, sizeof(buf), 0);
if (n < 0 && errno == EAGAIN) {
continue;
}
}
这会疯狂空转,CPU 直接飙高。
因为没有数据时,你一直问:
有数据吗?
没有。
有数据吗?
没有。
有数据吗?
没有。
所以非阻塞 I/O 通常要配合:
select / poll / epoll
也就是:
没有事件时睡眠;
有事件时再尝试读写。
这就是 epoll 的作用。
十一、 epoll 和非阻塞的关系
epoll 通常配合非阻塞 fd 使用。
典型流程:
把 socket 设为非阻塞
注册到 epoll
epoll_wait 等事件
有 EPOLLIN 再 recv
读到 EAGAIN 停止
继续 epoll_wait
代码结构像这样:
while (true) {
int n = epoll_wait(epfd, events, 1024, -1);
for (int i = 0; i < n; ++i) {
int fd = events[i].data.fd;
if (events[i].events & EPOLLIN) {
while (true) {
ssize_t ret = recv(fd, buf, sizeof(buf), 0);
if (ret > 0) {
// 处理数据
} else if (ret == 0) {
// 对端关闭
close(fd);
break;
} else {
if (errno == EAGAIN || errno == EWOULDBLOCK) {
break;
}
close(fd);
break;
}
}
}
}
}
这就是高性能网络编程里最经典的模式之一。