线程休眠引发UDP发送/接收延迟飙升的原因及优化方案咨询
我正在开展UDP环回性能实验,发现线程休眠会导致UDP最小延迟大幅飙升。分别用C和Rust编写了几乎相同的代码,在同一进程中启动两个线程,发送/接收UDP数据包并测量send和recv调用的耗时,已在macOS和Linux系统上完成测试。
实验发现,在循环中使用sleep()对请求/响应循环进行节流时,recv()和send()调用的耗时远高于正常水平。以下是客户端相关循环代码:
while (1) { // This sleep causes a large increase in latency #ifdef ENABLE_SLEEP usleep(10000); #endif double t_pre_send = curr_timestamp(); if (send(sockfd, NULL, 0, 0) == -1) { perror("send failed"); } avg_send_elapsed = 0.9*avg_send_elapsed + 0.1*(curr_timestamp() - t_pre_send); double t_pre_recv = curr_timestamp(); socklen_t len = 0; int num_bytes = recvfrom(sockfd, (char *)buffer, sizeof(buffer), 0, (struct sockaddr *) &cliaddr, &len); avg_recv_elapsed = 0.9*avg_recv_elapsed + 0.1*(curr_timestamp() - t_pre_recv); double curr_time = curr_timestamp(); if (curr_time - last_print > 1.0) { last_print = curr_time; printf("[%.1f] send: %.2fus recv: %.2fus\n", curr_time - t_start, avg_send_elapsed*1000000, avg_recv_elapsed*1000000); } }
无休眠时的测试结果
[1.0] send: 4.93us recv: 7.41us [2.0] send: 4.68us recv: 7.04us [3.0] send: 4.86us recv: 7.58us [4.0] send: 4.79us recv: 7.60us [5.0] send: 4.88us recv: 7.03us [6.0] send: 4.70us recv: 7.57us [7.0] send: 4.49us recv: 8.02us [8.0] send: 4.47us recv: 7.23us [9.0] send: 4.58us recv: 7.15us
启用休眠后的测试结果
[1.0] send: 23.85us recv: 102.13us [2.0] send: 35.41us recv: 78.07us [3.0] send: 70.47us recv: 141.07us [4.0] send: 29.90us recv: 107.35us [5.0] send: 45.17us recv: 194.27us [6.0] send: 32.49us recv: 117.74us [7.0] send: 32.25us recv: 117.83us [8.0] send: 35.48us recv: 85.67us [9.1] send: 33.86us recv: 108.71us
原本认为休眠只会调整每秒循环迭代次数,但不会影响send()和recvfrom()函数的调用耗时。请问:
- 为何UDP延迟会大幅上升?
- 有没有可在不产生延迟损失的情况下节流传输的技术?
一、延迟飙升的原因
1. 线程调度与CPU状态切换
调用usleep()后,操作系统会将线程从运行队列移除,放入等待队列。休眠结束后,线程需要重新被调度到CPU执行,这个调度过程本身就有微秒到毫秒级延迟。更关键的是,10ms的休眠足够让CPU进入低功耗休眠状态(如Intel的C-states),从低功耗状态唤醒CPU需要额外开销,直接拉高后续send/recv的调用耗时。
2. 内核资源调度倾斜
无休眠时,线程持续进行网络操作,内核会为其套接字和相关网络栈组件保持活跃状态,调度优先级也会更高。插入休眠后,内核会将资源倾向其他活跃线程,线程唤醒后需要重新等待网络资源调度、初始化相关状态,进一步增加系统调用耗时。
3. 计时器精度与调度抖动
usleep()的精度受操作系统计时器粒度限制(如Linux默认HZ=250,精度约4ms),实际休眠时间可能长于预期。此外,休眠结束后线程不一定能立即获得CPU时间片,需要等待其他线程执行完毕,这种调度抖动会放大延迟波动。
二、无延迟损失的节流技术
1. 自旋等待替代休眠
如果节流间隔较小(微秒级),可以用自旋等待避免线程被调度出去,虽然会占用CPU资源,但能避免调度和CPU唤醒开销:
#include <time.h> void spin_wait_us(long us) { struct timespec start, now; clock_gettime(CLOCK_MONOTONIC, &start); long elapsed; do { clock_gettime(CLOCK_MONOTONIC, &now); elapsed = (now.tv_sec - start.tv_sec)*1000000 + (now.tv_nsec - start.tv_nsec)/1000; } while (elapsed < us); }
2. 提升线程调度优先级
将发送/接收线程设置为实时调度优先级(Linux用SCHED_FIFO/SCHED_RR,macOS用pthread_setschedparam),确保线程唤醒后能立即获得CPU时间片,减少调度延迟。
3. 定时器+IO多路复用
利用内核定时器(如Linux的timerfd)结合epoll/kqueue,让线程在等待UDP事件的同时等待定时器,避免主动休眠带来的调度开销:
// 创建timerfd设置10ms间隔 int timer_fd = timerfd_create(CLOCK_MONOTONIC, TFD_NONBLOCK); struct itimerspec ts = { .it_interval = {0, 10*1000*1000}, .it_value = {0, 10*1000*1000} }; timerfd_settime(timer_fd, 0, &ts, NULL); // 将timer_fd加入epoll监听 struct epoll_event ev; ev.events = EPOLLIN; ev.data.fd = timer_fd; epoll_ctl(epoll_fd, EPOLL_CTL_ADD, timer_fd, &ev); // 等待定时器或UDP事件 epoll_wait(epoll_fd, &ev, 1, -1);
这种方式让线程处于可中断睡眠状态,既节省CPU,又能快速唤醒,同时可监听UDP接收事件,避免主动recvfrom的阻塞开销。
4. 批量发送/接收
如果业务允许,积累多个数据包后批量发送,减少系统调用频率,同时避免频繁休眠。通过批量操作平衡节流需求和延迟损失。
内容的提问来源于stack exchange,提问作者staticfloat

