C语言timeout如何实现?

99ANYc3cd6
预计阅读时长 31 分钟
位置: 首页 C语言 正文

标准I/O操作(如 read, write, scanf

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

c语言 timeout
(图片来源网络,侵删)

方法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() 的一个改进版本,它没有文件描述符数量的限制,接口也更清晰。

c语言 timeout
(图片来源网络,侵删)

原理: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;
}

网络编程(套接字操作)

对于网络套接字,除了上面提到的 selectpoll,还有更现代、更高效的解决方案。

方法3:使用 pselect() (POSIX标准)

pselect()select() 的一个线程安全版本,它使用 sigmask 来原子性地修改和恢复信号掩码,避免在信号处理和I/O等待之间产生竞态条件。

c语言 timeout
(图片来源网络,侵删)

方法4:使用 SO_RCVTIMEOSO_SNDTIMEO (特定于套接字)

这是最简单直接的方法,通过 setsockopt() 为套接字本身设置接收和发送超时。

原理: 在调用 connect(), recv(), send() 等函数前,设置套接字的超时选项,如果操作在指定时间内未完成,系统会直接返回错误码 EAGAINEWOULDBLOCK

示例代码: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多路复用技术,它比 selectpoll 效率更高。

原理: 通过 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 是最灵活和标准的方式。
-- 展开阅读全文 --
头像
C语言change是什么?如何实现?
« 上一篇 04-20
同级推荐文章,如何实现精准关联?
下一篇 » 04-20

相关文章

取消
微信二维码
支付宝二维码

目录[+]