PAYATHON 2026

如何避免信号与 pthread_cond_wait 之间的竞态?

数据阿明

结论

不能指望 pthread_cond_wait 因收到信号而返回 EINTR。POSIX 规定,线程在等待条件变量时收到信号,会继续等待或以 0 返回;收到信号本身不会让它返回 EINTR。条件变量等待可以因条件变量通知或允许的虚假唤醒而返回。

如果只需要等待停止信号,可以用 POSIX 信号量代替条件变量:在信号处理函数中调用 sem_post(),主线程调用 sem_wait()。信号量会保留已发出的通知,所以即使信号在主线程开始等待前到达,通知也不会丢失。

原代码中的竞态

原代码先检查 stopping,然后调用 pthread_cond_wait()。如果信号处理函数恰好在这两步之间把标志设为 1,而处理函数没有通知条件变量,主线程仍可能开始等待。之后若没有新的条件变量通知,它可能一直睡眠。

volatile 不能替代线程同步,也不能让 pthread_cond_wait() 按特定方式响应信号。volatile int 不是通用的线程间同步机制。即使改用 volatile sig_atomic_t,也无法避免检查标志后、开始等待前发生的通知丢失。

用信号量等待停止信号

下面的示例假设只有主线程处理 SIGINT,其他线程都屏蔽该信号。新线程会继承创建者当前的信号屏蔽集,因此应先屏蔽信号并安装处理函数,再创建其他线程,最后只在主线程中解除屏蔽。

#include <errno.h>
#include <pthread.h>
#include <semaphore.h>
#include <signal.h>

static sem_t stop_sem;
static volatile sig_atomic_t stopping = 0;

static void handle_sigint(int signo)
{
    (void)signo;
    stopping = 1;
    sem_post(&stop_sem);
}

int main(void)
{
    sigset_t set;
    struct sigaction sa = {0};

    sigemptyset(&set);
    sigaddset(&set, SIGINT);
    pthread_sigmask(SIG_BLOCK, &set, NULL);

    sem_init(&stop_sem, 0, 0);

    sa.sa_handler = handle_sigint;
    sigemptyset(&sa.sa_mask);
    sigaction(SIGINT, &sa, NULL);

    /* 在此创建其他线程;它们继承 SIGINT 屏蔽状态。 */

    pthread_sigmask(SIG_UNBLOCK, &set, NULL);

    while (!stopping) {
        if (sem_wait(&stop_sem) == 0)
            break;
        if (errno != EINTR)
            break;
    }

    /* 执行停止清理。 */
    sem_destroy(&stop_sem);
    return 0;
}

POSIX 规定,sem_post() 可以在信号处理函数中调用,因为它是异步信号安全函数。若信号在 sem_wait() 开始前到达,信号量计数会增加,之后调用 sem_wait() 时就能立即取得通知。若 sem_wait() 因信号返回 EINTR,循环会重新检查停止标志。

示例省略了错误处理。实际代码应检查 pthread_sigmask()、sem_init()、sigaction() 等调用的返回值,并按程序的清理流程处理失败情况。stopping 只应由接收该信号的主线程读取;若其他线程也要读取,应使用合适的线程同步方式。

如果还要等待条件变量上的其他事件

信号量可以通知线程停止,但线程不能同时阻塞等待普通条件变量和信号量。如果程序还要响应条件变量上的其他工作事件,就需要重新设计通知方式,例如把相关事件汇入同一个等待机制,或采用定时等待,定期检查停止标志。定时轮询能避免永久睡眠,但会带来检测延迟。

在 Linux 上,也可以考虑用 signalfd 将信号转成文件描述符事件,再通过 poll 或 epoll 等待。这是平台相关的方案,不能让 pthread_cond_wait() 直接等待信号。

备注:内容仅供参考。