在谈论进程这个概念时,很容易问到进程的地址空间,然后很容易问到全局变量并发访问控制。 先说结论,并发机制有:多线程,信号处理,异步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;
}
🧪 执行流程分析:
-
程序开始运行,注册了
SIGINT的信号处理函数。 -
主线程设置
in_use = 1,模拟对buffer的占用。 -
如果此时你按下
Ctrl+C发送SIGINT:- 信号处理函数尝试检查
in_use并修改buffer。 - 但由于主流程刚刚设置完
in_use = 1,还没来得及修改 buffer,信号处理函数就介入了。
- 信号处理函数尝试检查
-
此时
in_use == 1,信号处理函数打印"buffer is in use",看起来没问题。 -
但如果信号发生在
in_use = 1和strcpy()之间?或者在strcpy()执行过程中?- 就可能导致
strcpy()被中断,出现缓冲区部分写入、乱码等问题。 - 即使我们有
in_use标志,也无法完全避免竞争条件,因为不是原子操作。
- 就可能导致
🛡️ 为什么这是“并发问题”?
尽管这是一个单线程、单进程程序,但由于信号处理函数的执行是异步中断式的,它可能会:
- 在任意指令点打断主流程;
- 访问共享变量(如
buffer,in_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 回调 | ✅ 视情况 |
即使是单线程、单进程程序,只要引入了异步机制(如信号、定时器、中断处理等),就必须考虑并发问题。