并发机制

97 阅读3分钟

在谈论进程这个概念时,很容易问到进程的地址空间,然后很容易问到全局变量并发访问控制。 先说结论,并发机制有:多线程,信号处理,异步IO,中断处理;这些情况下要考虑并发控制。

多线程不用说,典型的并发机制;
信号处理: 即使是在单线程单进程的应用中,如果使用了信号处理机制,仍然可能会引入类似并发的行为。这是因为在接收到信号时,信号处理函数会被调用,这会中断正常的执行流程去执行信号处理函数中的代码。如果信号处理函数和主程序共享某些资源(如全局变量),那么就可能出现竞态条件,导致数据不一致或其他并发问题。

主流程与信号处理函数访问共享数据导致数据不一致

c
深色版本
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <string.h>

char buffer[64] = "Initial message";
int in_use = 0; // 表示 buffer 是否正在被使用

void handle_sigint(int sig) {
    if (!in_use) {
        strcpy(buffer, "SIGINT received"); // 不安全操作!
        printf("Signal handler modified buffer\n");
    } else {
        printf("Signal handler: buffer is in use, skipping\n");
    }
}

int main() {
    struct sigaction sa;
    memset(&sa, 0, sizeof(sa));
    sa.sa_handler = handle_sigint;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = 0;

    sigaction(SIGINT, &sa, NULL);

    printf("Main thread starting to modify buffer...\n");

    in_use = 1;
    sleep(1);  // 模拟耗时操作
    strcpy(buffer, "Main thread message"); // 修改 buffer
    sleep(1);
    in_use = 0;

    printf("Final buffer content: %s\n", buffer);
    return 0;
}

🧪 执行流程分析:

  1. 程序开始运行,注册了 SIGINT 的信号处理函数。

  2. 主线程设置 in_use = 1,模拟对 buffer 的占用。

  3. 如果此时你按下 Ctrl+C 发送 SIGINT

    • 信号处理函数尝试检查 in_use 并修改 buffer
    • 但由于主流程刚刚设置完 in_use = 1,还没来得及修改 buffer,信号处理函数就介入了。
  4. 此时 in_use == 1,信号处理函数打印 "buffer is in use",看起来没问题。

  5. 但如果信号发生在 in_use = 1 和 strcpy() 之间?或者在 strcpy() 执行过程中?

    • 就可能导致 strcpy() 被中断,出现缓冲区部分写入、乱码等问题。
    • 即使我们有 in_use 标志,也无法完全避免竞争条件,因为不是原子操作。

🛡️ 为什么这是“并发问题”?

尽管这是一个单线程、单进程程序,但由于信号处理函数的执行是异步中断式的,它可能会:

  • 在任意指令点打断主流程;
  • 访问共享变量(如 bufferin_use);
  • 导致主流程的数据处于不一致状态;
  • 引发不可预料的行为(比如崩溃、死锁、数据损坏);

这就是典型的并发问题中的竞态条件(Race Condition)


✅ 如何安全地处理这种情况?

方法一:使用 sig_atomic_t 类型的变量进行简单同步(适用于非常简单的标志)

c
深色版本
volatile sig_atomic_t flag = 0;

void handle_sigint(int sig) {
    flag = 1;
}

只能用于简单的整型标志,不能用于复杂结构。


方法二:使用信号掩码阻塞信号,在关键区域屏蔽中断

c
深色版本
sigset_t mask, oldmask;
sigemptyset(&mask);
sigaddset(&mask, SIGINT);
sigprocmask(SIG_BLOCK, &mask, &oldmask);

// 关键代码段(保护 buffer 操作)
strcpy(buffer, "Main thread message");

sigprocmask(SIG_SETMASK, &oldmask, NULL); // 恢复信号

这样可以确保在关键代码段期间不会被信号中断。


方法三:延迟处理信号(使用 signalfd + epoll/io_uring)

在现代编程中,推荐将信号处理从异步中断方式改为同步事件处理方式,例如使用 signalfd 结合 epoll,这样就不会打断主线程流程,从根本上避免并发问题。


✅ 总结

场景是否可能产生并发问题
单线程、无信号、无异步❌ 否
单线程、使用信号处理函数✅ 可能
多线程、共享资源✅ 是
单线程、使用异步 I/O 回调✅ 视情况

即使是单线程、单进程程序,只要引入了异步机制(如信号、定时器、中断处理等),就必须考虑并发问题。