C标准中函数指针变量是否具备原子性与线程安全性?
问题:C标准中函数指针是否具备原子性?作为函数指针的变量是否具备线程安全性?这里的线程安全性指变量的值始终是确定的合法值(即某一时刻赋值的现有函数地址)。我认为对该变量的赋值与读取操作都是单条汇编指令,基于此应该是安全的,对吗?注:针对单核心CPU。
示例代码
Test.h
typedef int fn_t (int a); void set_a(fn_t * fn); void do_something(void);
Test.c
static fn_t * a; void set_a(fn_t * fn){ a = fn; } void do_something(void){ if (a != NULL && a(1) == 0){ printf(" Happend \n"); } printf("Doing something else \n"); }
核心结论与分析
C标准层面的原子性定义
C标准(包括C11及以后版本)没有将普通函数指针的赋值/读取操作列为原子操作。原子操作的规范仅在<stdatomic.h>中定义,只有显式声明为_Atomic类型的变量,其指定操作才会被标准保证为原子的。普通函数指针变量的读写属于普通内存操作,标准不保证其原子性,也不保证线程安全。单核心CPU下的实际行为
在单核心CPU架构中,线程通过时间片轮转切换执行——同一时刻只有一个线程在运行,指令不会被中途打断。对于大多数常见架构(如x86、ARM),函数指针的大小等于CPU的字长(32位系统为4字节,64位为8字节),这类指针的赋值/读取通常会被编译为单条汇编指令(比如x86的mov指令)。这种情况下,确实不会出现“部分写入”的情况:读取操作要么拿到完整的旧值,要么拿到完整的新值,不会读到半更新的非法地址。
但要注意:这是特定硬件和编译器的实现细节,不是C标准的保证。如果换用一些特殊架构(比如指针长度超过CPU字长的罕见平台),指针读写可能会拆分为多条指令,此时即使单核心也可能出现中间状态。
- 示例代码的潜在问题
即使单核心下指针读写是原子的,你的示例代码仍然存在线程安全隐患:
在do_something()的if (a != NULL && a(1) == 0)判断中,a != NULL的检查和a(1)的调用是两个独立的操作。假设线程1执行完a != NULL(此时a非空),然后时间片切换到线程2,线程2调用set_a(NULL)将a置空,接着切回线程1执行a(1),此时就会触发空指针调用,导致程序崩溃。
这种问题属于竞态条件,和指针读写的原子性无关,需要通过同步机制(比如互斥锁pthread_mutex_t)来保护对a的访问和调用序列。
总结
- 若依赖C标准的可移植性,必须显式使用
_Atomic修饰函数指针,或用同步机制保护读写; - 单核心CPU的常见架构下,普通函数指针读写通常是原子的,但这是实现细节,不能作为可移植代码的依据;
- 示例代码的逻辑需要额外同步,否则即使原子读写也会出现竞态问题。
内容的提问来源于stack exchange,提问作者Socialist2

