You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

线程休眠引发UDP发送/接收延迟飙升的原因及优化方案咨询

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()函数的调用耗时。请问:

  1. 为何UDP延迟会大幅上升?
  2. 有没有可在不产生延迟损失的情况下节流传输的技术?

问题解答

一、延迟飙升的原因

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.05 20:40:18