标准I/O操作(如 read, write, scanf)
标准C库本身不直接提供带超时的I/O函数,我们需要借助操作系统的特定功能来实现。

方法1:使用 select() (POSIX标准,跨平台)
select() 是一个经典的I/O多路复用函数,可以用来监视一组文件描述符(File Descriptors, FDs)的状态变化,包括是否可读、可写或发生异常,我们可以利用它来实现I/O超时。
原理:
select() 的第二个参数 readfds 用于监视可读的文件描述符,我们将需要监视的FD(stdin 或一个套接字)加入 readfds,然后设置一个超时时间,如果在超时时间内该FD变为可读,select() 返回;否则,超时后返回0。
示例代码:read 超时
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/select.h>
#include <time.h>
#define TIMEOUT_SEC 5 // 超时时间5秒
int read_with_timeout(int fd, void *buffer, size_t count) {
fd_set read_fds;
struct timeval timeout;
// 初始化文件描述符集合
FD_ZERO(&read_fds);
FD_SET(fd, &read_fds);
// 设置超时时间
timeout.tv_sec = TIMEOUT_SEC;
timeout.tv_usec = 0;
// 调用select
int ret = select(fd + 1, &read_fds, NULL, NULL, &timeout);
if (ret < 0) {
// select出错
perror("select");
return -1;
} else if (ret == 0) {
// select超时
printf("Timeout: No data received for %d seconds.\n", TIMEOUT_SEC);
return 0; // 返回0表示超时
} else {
// 数据可读
if (FD_ISSET(fd, &read_fds)) {
return read(fd, buffer, count);
}
}
return -1; // 理论上不会执行到这里
}
int main() {
char ch;
printf("Please input a character (will timeout in 5 seconds):\n");
int n = read_with_timeout(STDIN_FILENO, &ch, 1);
if (n > 0) {
printf("You entered: %c\n", ch);
} else if (n == 0) {
printf("Operation timed out.\n");
} else {
printf("An error occurred.\n");
}
return 0;
}
方法2:使用 poll() (POSIX标准,比select更现代)
poll() 是 select() 的一个改进版本,它没有文件描述符数量的限制,接口也更清晰。

原理:
与 select() 类似,但使用 struct pollfd 数组来监视FDs,同样设置超时时间。
示例代码:poll 实现超时
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <poll.h>
#define TIMEOUT_SEC 5
int read_with_poll(int fd, void *buffer, size_t count) {
struct pollfd fds;
fds.fd = fd;
fds.events = POLLIN; // 关心可读事件
int ret = poll(&fds, 1, TIMEOUT_SEC * 1000); // 超时单位是毫秒
if (ret < 0) {
perror("poll");
return -1;
} else if (ret == 0) {
printf("Timeout: poll timed out after %d seconds.\n", TIMEOUT_SEC);
return 0;
} else {
if (fds.revents & POLLIN) {
return read(fd, buffer, count);
}
}
return -1;
}
int main() {
char ch;
printf("Please input a character (will timeout in 5 seconds):\n");
int n = read_with_poll(STDIN_FILENO, &ch, 1);
if (n > 0) {
printf("You entered: %c\n", ch);
} else if (n == 0) {
printf("Operation timed out.\n");
} else {
printf("An error occurred.\n");
}
return 0;
}
网络编程(套接字操作)
对于网络套接字,除了上面提到的 select 和 poll,还有更现代、更高效的解决方案。
方法3:使用 pselect() (POSIX标准)
pselect() 是 select() 的一个线程安全版本,它使用 sigmask 来原子性地修改和恢复信号掩码,避免在信号处理和I/O等待之间产生竞态条件。

方法4:使用 SO_RCVTIMEO 和 SO_SNDTIMEO (特定于套接字)
这是最简单直接的方法,通过 setsockopt() 为套接字本身设置接收和发送超时。
原理:
在调用 connect(), recv(), send() 等函数前,设置套接字的超时选项,如果操作在指定时间内未完成,系统会直接返回错误码 EAGAIN 或 EWOULDBLOCK。
示例代码:recv 超时
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <errno.h>
#define PORT 8080
#define TIMEOUT_SEC 3
int main() {
int sock = 0;
struct sockaddr_in serv_addr;
char *hello = "Hello from client";
char buffer[1024] = {0};
// 创建套接字
if ((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
printf("\n Socket creation error \n");
return -1;
}
// 设置套接字超时
struct timeval tv;
tv.tv_sec = TIMEOUT_SEC;
tv.tv_usec = 0;
if (setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, (const char*)&tv, sizeof(tv)) < 0) {
perror("setsockopt SO_RCVTIMEO failed");
return -1;
}
if (setsockopt(sock, SOL_SOCKET, SO_SNDTIMEO, (const char*)&tv, sizeof(tv)) < 0) {
perror("setsockopt SO_SNDTIMEO failed");
return -1;
}
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(PORT);
// 将IP地址从文本转换为网络格式
if(inet_pton(AF_INET, "127.0.0.1", &serv_addr.sin_addr)<=0) {
printf("\nInvalid address/ Address not supported \n");
return -1;
}
// 连接服务器
if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) {
// 注意:这里可能会因为服务器未启动而超时
if (errno == EINPROGRESS || errno == ETIMEDOUT) {
printf("Connection timed out.\n");
} else {
perror("Connection failed");
}
return -1;
}
send(sock, hello, strlen(hello), 0);
printf("Hello message sent\n");
int valread = recv(sock, buffer, 1024, 0);
if (valread < 0) {
if (errno == EAGAIN || errno == EWOULDBLOCK) {
printf("recv() timed out.\n");
} else {
perror("recv() failed");
}
} else {
printf("Server: %s\n", buffer);
}
close(sock);
return 0;
}
方法5:使用 epoll (Linux特有,高性能)
对于需要同时处理大量连接的高性能服务器程序,epoll 是Linux下首选的I/O多路复用技术,它比 select 和 poll 效率更高。
原理:
通过 epoll_wait() 来等待事件,并可以指定超时时间。
多线程/多进程
当你的任务不是简单的I/O操作,而是一个任意的计算任务时,可以使用多线程或多进程来实现超时。
方法6:使用 pthread + pthread_cond_timedwait (POSIX线程)
原理:
创建一个工作线程来执行耗时任务,主线程创建一个条件变量和关联的互斥锁,然后调用 pthread_cond_timedwait 进行等待,并设置一个绝对超时时间,工作线程完成任务后,会通过 pthread_cond_signal 唤醒主线程,如果超时,主线程会从 pthread_cond_timedwait 返回错误码 ETIMEDOUT。
示例代码:任务超时
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <time.h>
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
int task_completed = 0;
void* long_running_task(void* arg) {
printf("Task thread: Starting task...\n");
sleep(10); // 模拟一个耗时10秒的任务
printf("Task thread: Task finished!\n");
pthread_mutex_lock(&mutex);
task_completed = 1;
pthread_mutex_unlock(&mutex);
pthread_cond_signal(&cond); // 通知主线程任务完成
return NULL;
}
int main() {
pthread_t thread_id;
// 创建工作线程
if (pthread_create(&thread_id, NULL, long_running_task, NULL) != 0) {
perror("Failed to create thread");
return 1;
}
printf("Main thread: Waiting for task with a 5-second timeout...\n");
pthread_mutex_lock(&mutex);
// 计算超时时间 (绝对时间)
struct timespec ts;
clock_gettime(CLOCK_REALTIME, &ts);
ts.tv_sec += 5; // 5秒后超时
// 等待条件变量,带超时
int wait_ret = pthread_cond_timedwait(&cond, &mutex, &ts);
pthread_mutex_unlock(&mutex);
if (wait_ret == 0) {
if (task_completed) {
printf("Main thread: Task completed successfully.\n");
} else {
printf("Main thread: Woken up for unknown reason.\n");
}
} else if (wait_ret == ETIMEDOUT) {
printf("Main thread: Task timed out after 5 seconds.\n");
// 可以在这里决定是否取消线程
pthread_cancel(thread_id);
} else {
perror("Main thread: pthread_cond_timedwait failed");
}
// 等待线程结束(如果未被取消)
pthread_join(thread_id, NULL);
pthread_mutex_destroy(&mutex);
pthread_cond_destroy(&cond);
return 0;
}
总结与对比
| 方法 | 适用场景 | 优点 | 缺点 | 平台 |
|---|---|---|---|---|
select() |
标准I/O、网络套接字 | 跨平台,概念简单 | FD数量受限,性能随FD数量增加而下降 | POSIX, Windows |
poll() |
标准I/O、网络套接字 | 无FD数量限制,接口更清晰 | 性能仍不如epoll |
POSIX, Windows |
SO_RCVTIMEO |
仅网络套接字 | 使用简单,直接作用于套接字 | 仅适用于套接字 | Linux, macOS, Windows |
epoll() |
高性能网络服务器 | Linux下性能极高,无FD数量限制 | 仅限Linux | Linux |
pthread_cond_timedwait |
任意计算任务 | 通用性强,可用于任何阻塞操作 | 需要线程管理,增加了复杂性 | POSIX |
如何选择?
- 简单的I/O超时(如控制台输入):优先使用
poll(),它比select()更现代。 - 网络编程:
- 如果追求简单和跨平台,
poll()是个好选择。 - 如果是在Linux上开发高性能服务器,
epoll()是不二之选。 - 如果只是想快速给某个网络操作加个超时,
setsockopt()是最直接的方法。
- 如果追求简单和跨平台,
- 通用任务超时(非I/O):使用多线程和
pthread_cond_timedwait是最灵活和标准的方式。
