如何避免信号与 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() 直接等待信号。
备注:内容仅供参考。
