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

C/C++中SIGINT在条件检查与阻塞调用间触发的处理方案咨询

解决线程A阻塞调用的竞态唤醒问题

这个竞态场景的核心矛盾是:线程A检查中断标志后、进入阻塞调用前,线程B发送的SIGINT会被处理并设置标志,但线程A已经错过了检查,依然会进入阻塞状态。我们可以通过信号掩码控制+双重检查机制来彻底消除这个竞态窗口,完全不需要修改阻塞函数的实现,以下是具体方案:

核心思路

  1. 阻塞信号隔离竞态窗口:线程A在检查中断标志时,先阻塞SIGINT,让线程B发送的信号处于pending状态,不会在检查和进入阻塞之间被处理。
  2. 双重检查标志:第一次检查在信号阻塞状态下;解除信号阻塞后立刻做第二次检查,此时pending的信号会被立即处理,若标志已设置则直接退出,不进入阻塞调用。
  3. 利用EINTR返回值:阻塞调用被SIGINT中断时会返回-1且errno=EINTR,此时再次检查标志决定是否退出循环。
  4. 安全的标志类型:使用volatile sig_atomic_t作为中断标志,保证信号处理函数中的修改是原子操作且对线程可见。

C语言实现

#include <pthread.h>
#include <signal.h>
#include <errno.h>
#include <unistd.h>
#include <stdio.h>

// 中断标志:必须用volatile sig_atomic_t保证信号安全
volatile sig_atomic_t is_interrupted = 0;

// SIGINT信号处理函数:仅修改标志,不做其他耗时/非安全操作
void sigint_handler(int sig) {
    is_interrupted = 1;
}

// 模拟用户无法修改的阻塞调用:仅能被SIGINT唤醒,返回-1且errno=EINTR
int block() {
    printf("Entering block()...\n");
    // pause()会阻塞直到收到信号,被SIGINT中断时返回-1,errno=EINTR
    return pause();
}

// 线程A的执行逻辑
void* thread_a_func(void* arg) {
    sigset_t sigint_mask, orig_mask;
    sigemptyset(&sigint_mask);
    sigaddset(&sigint_mask, SIGINT);

    // 初始化:阻塞SIGINT,避免信号在检查标志时触发
    pthread_sigmask(SIG_BLOCK, &sigint_mask, &orig_mask);

    // 注册SIGINT信号处理函数
    struct sigaction sa;
    sa.sa_handler = sigint_handler;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = 0;
    sigaction(SIGINT, &sa, NULL);

    while (1) {
        // 第一次检查:此时SIGINT被阻塞,信号不会被处理
        if (is_interrupted) {
            printf("Thread A: Interrupted, exiting loop.\n");
            break;
        }

        // 临时解除SIGINT阻塞,允许信号递达
        pthread_sigmask(SIG_UNBLOCK, &sigint_mask, NULL);

        // 第二次检查:此时pending的信号已被处理,若标志已设置则直接退出
        if (is_interrupted) {
            pthread_sigmask(SIG_BLOCK, &sigint_mask, NULL);
            printf("Thread A: Detected pending interrupt, exiting loop.\n");
            break;
        }

        // 调用阻塞函数
        int ret = block();

        // 重新阻塞SIGINT,隔离下一次检查的竞态窗口
        pthread_sigmask(SIG_BLOCK, &sigint_mask, NULL);

        // 处理阻塞调用的返回结果
        if (ret == -1 && errno == EINTR) {
            printf("Thread A: block() interrupted by SIGINT.\n");
            // 再次检查标志,确认是否需要退出
            if (is_interrupted) {
                break;
            }
        } else {
            printf("Thread A: block() completed normally.\n");
        }
    }

    // 恢复线程初始的信号掩码
    pthread_sigmask(SIG_SETMASK, &orig_mask, NULL);
    return NULL;
}

// 线程B的执行逻辑:发送SIGINT唤醒线程A
void* thread_b_func(void* arg) {
    pthread_t* thread_a = (pthread_t*)arg;
    printf("Thread B: Waiting 2 seconds before sending SIGINT...\n");
    sleep(2);
    // 向线程A发送SIGINT
    pthread_kill(*thread_a, SIGINT);
    return NULL;
}

int main() {
    pthread_t thread_a, thread_b;
    pthread_create(&thread_a, NULL, thread_a_func, NULL);
    pthread_create(&thread_b, NULL, thread_b_func, &thread_a);

    pthread_join(thread_a, NULL);
    pthread_join(thread_b, NULL);
    return 0;
}

C++实现

C的实现逻辑和C一致,只是用std::thread替代pthread API,信号处理部分依然使用POSIX信号机制(C标准库对信号的原生支持有限):

#include <thread>
#include <signal.h>
#include <errno.h>
#include <unistd.h>
#include <iostream>

volatile sig_atomic_t is_interrupted = 0;

void sigint_handler(int sig) {
    is_interrupted = 1;
}

// 模拟用户无法修改的阻塞调用
int block() {
    std::cout << "Entering block()..." << std::endl;
    return pause();
}

void thread_a_func() {
    sigset_t sigint_mask, orig_mask;
    sigemptyset(&sigint_mask);
    sigaddset(&sigint_mask, SIGINT);

    // 阻塞SIGINT
    pthread_sigmask(SIG_BLOCK, &sigint_mask, &orig_mask);

    // 注册信号处理函数
    struct sigaction sa;
    sa.sa_handler = sigint_handler;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = 0;
    sigaction(SIGINT, &sa, nullptr);

    while (true) {
        if (is_interrupted) {
            std::cout << "Thread A: Interrupted, exiting loop." << std::endl;
            break;
        }

        pthread_sigmask(SIG_UNBLOCK, &sigint_mask, nullptr);

        if (is_interrupted) {
            pthread_sigmask(SIG_BLOCK, &sigint_mask, nullptr);
            std::cout << "Thread A: Detected pending interrupt, exiting loop." << std::endl;
            break;
        }

        int ret = block();

        pthread_sigmask(SIG_BLOCK, &sigint_mask, nullptr);

        if (ret == -1 && errno == EINTR) {
            std::cout << "Thread A: block() interrupted by SIGINT." << std::endl;
            if (is_interrupted) {
                break;
            }
        } else {
            std::cout << "Thread A: block() completed normally." << std::endl;
        }
    }

    pthread_sigmask(SIG_SETMASK, &orig_mask, nullptr);
}

void thread_b_func(std::thread& thread_a) {
    std::cout << "Thread B: Waiting 2 seconds before sending SIGINT..." << std::endl;
    sleep(2);
    // 通过native_handle获取pthread_t,发送SIGINT
    pthread_kill(thread_a.native_handle(), SIGINT);
}

int main() {
    std::thread thread_a(thread_a_func);
    std::thread thread_b(thread_b_func, std::ref(thread_a));

    thread_a.join();
    thread_b.join();
    return 0;
}

关键注意事项

  • 信号处理函数的安全性:信号处理函数中只能调用异步信号安全的函数(如修改sig_atomic_t变量),禁止调用printf、malloc等非安全函数(示例中为了演示加入了打印,实际生产环境应移除)。
  • 信号掩码的严格控制:每次检查标志前必须阻塞SIGINT,调用阻塞函数前解除阻塞,调用后重新阻塞,确保竞态窗口被完全隔离。
  • volatile的必要性:volatile关键字防止编译器优化标志变量的读取,确保线程每次都从内存中获取最新值。

内容的提问来源于stack exchange,提问作者Kelner

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.14 07:17:07