编辑
2025-12-22
记录知识
0
请注意,本文编写于 138 天前,最后修改于 138 天前,其中某些信息可能已经过时。

目录

volatile 的作用
示例
总结
参考

很多C语言开发者经常错误的使用 volatile 关键字,本文用于矫正 volatile 的错误进行说明

volatile 的作用

volatile加在变量前面,可以告诉编译器,此变量可能被意外的修改,所以要求编译器每次获取改值的时候,总是从内存拿最新的那份。

可以看到,volatile 本身就是在抑制编译器的正向优化,这就使得加上volatile关键字的程序性能就会偏低,这还是其次,更重要的事情是 volatile 并不能保证CPU不会重排它,它和内存栅栏是完全不同的概念。

也就是说,如果CPU乱序重排,那么程序就会带来意想不到的问题。本文就是介绍 volatile 带来的程序问题

示例

《高性能无锁编程-Dekker算法》介绍了高性能的无锁编程的一种思路,这种办法在业界已经广泛得到认可。它通过设置了flag和turn来实施“孔融让梨”的思想。

那是不是意味着,flag和turn变量用 volatile 让编译器每次从内存拿最新的数据就能有效避免flag和turn数据不一致的情况呢?

关于flag和turn数据可能不一致的情况说明,可以进一步看《不可忽视的cacheline问题》文章

先说结论: volatile 只能保证获取内存最新值,但是如果load/store 可以被CPU重排,那么每次load操作读取到的仍是旧值,这样代码就会出现问题。

所以千万要注意,volatile 和 内存栅栏 不是一回事。内存栅栏能够有效的隔绝CPU和内存的介限(DMB/DSB/ISB),而 volatile 只能保证读取最新内存的值。

下面查看演示代码

#include <pthread.h> #include <stdio.h> #include <stdlib.h> static volatile int flag0 = 0, flag1 = 0, turn = 1; static volatile int counter = 0; int test_case = 0; int loop_cnt; static void dekker0(void) { flag0 = 1; turn = 1; if (test_case == 1) __atomic_thread_fence(__ATOMIC_SEQ_CST); if (test_case == 2) __asm__ __volatile__("dmb ish" ::: "memory"); while ((flag1 == 1) && (turn == 1)); counter++; flag0 = 0; } static void dekker1(void) { flag1 = 1; turn = 0; if (test_case == 1) __atomic_thread_fence(__ATOMIC_SEQ_CST); if (test_case == 2) __asm__ __volatile__("dmb ish" ::: "memory"); while ((flag0 == 1) && (turn == 0)); counter++; flag1 = 0; } static void *task0(void *arg) { printf("Starting %s\n", __func__); for (int i = loop_cnt; i > 0; i--) dekker0(); return NULL; } static void *task1(void *arg) { printf("Starting %s\n", __func__); for (int i = loop_cnt; i > 0; i--) dekker1(); return NULL; } int main(int argc, char **argv) { pthread_t thread1, thread2; if (argc != 3) { fprintf(stderr, "Usage: %s <loopcount> <0(none)/1(c11_fence)/2(arm64_dmb)>\n", argv[0]); exit(1); } loop_cnt = atoi(argv[1]); test_case = atoi(argv[2]); int expected_sum = 2 * loop_cnt; (void) pthread_create(&thread1, NULL, task0, NULL); (void) pthread_create(&thread2, NULL, task1, NULL); void *ret; (void) pthread_join(thread1, &ret); (void) pthread_join(thread2, &ret); printf("Both threads terminated\n"); /* Check result */ if (counter != expected_sum) { printf("[-] Dekker did not work, sum %d rather than %d.\n", counter, expected_sum); printf("%d missed updates due to memory consistency races.\n", (expected_sum - counter)); return 1; } printf("[+] Dekker worked.\n"); return 0; }

编译

gcc volatile_bug.c -o volatile_bug

运行

# ./volatile_bug 1000000 0 Starting task0 Starting task1 Both threads terminated [-] Dekker did not work, sum 1999986 rather than 2000000. 14 missed updates due to memory consistency races.

可以看到,此代码运行多次,dekker算法好像直接失效了。其主要原因是volatile关键字的错误理解

解决此问题可以使用c11的thread fence进行内存同步,它相当于构造了内存栅栏,强制同步了内存。

__atomic_thread_fence(__ATOMIC_SEQ_CST);

此时能够保证CPU不再乱序(因为内存栅栏的存在),所以代码运行如下

# ./volatile_bug 1000000 1 Starting task1 Starting task0 Both threads terminated [+] Dekker worked.

如果在arm平台使用,应该使用

dmb + ish

dmb构造了内存栅栏,而ish让所有CPU的视角上内存表现一致。 所以其运行结果如下

# ./volatile_bug 1000000 2 Starting task0 Starting task1 Both threads terminated [+] Dekker worked.

总结

本文介绍了 volatile 关键字非常注意的两个点

  1. 性能低下
  2. 不能保证CPU乱序

所以在开发程序的时候,尽可能的避免volatile关键字。

同样的,内核也在极力的纠正大部分开发者错误的使用 volatile 关键字,详情可参考下面的连接

参考

内核并不建议使用 volatile 关键字:

https://www.kernel.org/doc/Documentation/process/volatile-considered-harmful.rst

dmb:

https://www.scs.stanford.edu/~zyedidia/arm64/dmb.html