java多线程编程实例中什么情况下需要加 volatile

多线程编程中什么情况下需要加 volatile?
我的图书馆
多线程编程中什么情况下需要加 volatile?
arrow-down
conversations
best_answerer
verified_and_best_answerer
live_emoji
与世界分享你的知识、经验和见解22 个回答码见 / RetroGame / 日麻 / LoveLiveC/C++多线程编程中不要使用volatile。(注:这里的意思指的是指望volatile解决多线程竞争问题是有很大风险的,除非所用的环境系统不可靠才会为了保险加上volatile,或者是从极限效率考虑来实现很底层的接口。这要求编写者对程序逻辑走向很清楚才行,不然就会出错)C++11标准中明确指出解决多线程的数据竞争问题应该使用原子操作或者互斥锁。C和C++中的volatile并不是用来解决多线程竞争问题的,而是用来修饰一些因为程序不可控因素导致变化的变量,比如访问底层硬件设备的变量,以提醒编译器不要对该变量的访问擅自进行优化。多线程场景下可以参考《Programming with POSIX threads》的作者Dave Butenhof对Why don't I need to declare shared variables VOLATILE?这个问题的解释:简单的来说,对访问共享数据的代码块加锁,已经足够保证数据访问的同步性,再加volatile完全是多此一举。如果光对共享变量使用volatile修饰而在可能存在竞争的操作中不加锁或使用原子操作对解决多线程竞争没有任何卵用,因为volatile并不能保证操作的原子性,在读取、写入变量的过程中仍然可能被其他线程打断导致意外结果发生。相关 Live 推荐 ??相关问题 116 个回答 139 个回答 11 个回答 10 个回答 87 个回答 96 个回答<div data-state='{&env&:{&isWechat&:false,&isQQNews&:false,&isAppView&:false},&timestamp&:8,&currentUser&:null,&questions&:{&&:{&status&:{&isLocked&:false,&isClose&:false,&unnormalDetails&:null,&isEvaluate&:false,&isSuggest&:false},&isTop&:false,&author&:{&avatarUrlTemplate&:&/da8e974dc_{size}.jpg&,&type&:&people&,&name&:&知乎用户&,&url&:&&,&urlToken&:null,&userType&:&people&,&isAdvertiser&:false,&avatarUrl&:&/da8e974dc_is.jpg&,&isOrg&:false,&headline&:&&,&badge&:[],&id&:&bce673d24c9&},&url&:&&,&title&:&多线程编程中什么情况下需要加 volatile?&,&detail&:&网上说 volatile 有一种使用是作为「多线程被几个任务共享变量的修饰符」。但是我看过很多代码多线程临界区里面变量声明时并没有加 volatile。比方说印象中整个 muduo 网络库好像就 atomicInt 有 volatile;nginx 中一些时间的表示都用了 volatile。是否意味着在可重入(线程安全且不互斥使用)函数的访问中的变量才需要用 volatile 呢?但是似乎有些代码就是拿了个全局的 int 之类的当做多线程的标识变量,不加 volatile 可行么?总之多线程编程中什么情况下需要加 volatile?如果说 volatile 关键字对多线程无用,那为什么类似 muduo 和 nginx 的库都有用到呢?相关问题:&,&answerCount&:22,&commentCount&:3,&questionType&:&normal&,&visitCount&:18232,&type&:&question&,&id&:,&isNormal&:true}},&answers&:{&&:{&author&:{&avatarUrlTemplate&:&/82069fddf_{size}.jpg&,&type&:&people&,&name&:&Gomo Psivarh&,&url&:&&,&badge&:[],&userType&:&people&,&isAdvertiser&:false,&avatarUrl&:&/DownloadImg/1/.jpg&,&isOrg&:false,&headline&:&码见 / RetroGame / 日麻 / LoveLive&,&urlToken&:&gomopsivarh&,&id&:&ec456de4a820d6c4f53fc&},&url&:&&,&question&:{&url&:&&,&type&:&question&,&id&:,&questionType&:&normal&,&title&:&多线程编程中什么情况下需要加 volatile?&},&excerpt&:&C/C++多线程编程中不要使用volatile。 (注:这里的意思指的是指望volatile解决多线程竞争问题是有很大风险的,除非所用的环境系统不可靠才会为了保险加上volatile,或者是从极限效率考虑来实现很底层的接口。这要求编写者对程序逻辑走向很清楚才行,不然就会出错) C++11标准中明确指出解决多线程的数据竞争问题应该使用原子操作或者互斥锁。 C和C++中的volatile并不是用来解决多线程竞争问题的,而是用来修饰一些因为程序不…&,&suggestEdit&:{&status&:false,&title&:&&,&url&:&&,&tip&:&&,&reason&:&&,&unnormalDetails&:null},&content&:&C/C++多线程编程中不要使用volatile。(注:这里的意思指的是指望volatile解决多线程竞争问题是有很大风险的,除非所用的环境系统不可靠才会为了保险加上volatile,或者是从极限效率考虑来实现很底层的接口。这要求编写者对程序逻辑走向很清楚才行,不然就会出错)C++11标准中明确指出解决多线程的数据竞争问题应该使用原子操作或者互斥锁。C和C++中的volatile并不是用来解决多线程竞争问题的,而是用来修饰一些因为程序不可控因素导致变化的变量,比如访问底层硬件设备的变量,以提醒编译器不要对该变量的访问擅自进行优化。多线程场景下可以参考《Programming with POSIX threads》的作者Dave Butenhof对Why don' t="" i="" need="" to="" declare="" shared="" variables="">这个问题的解释:简单的来说,对访问共享数据的代码块加锁,已经足够保证数据访问的同步性,再加volatile完全是多此一举。如果光对共享变量使用volatile修饰而在可能存在竞争的操作中不加锁或使用原子操作对解决多线程竞争没有任何卵用,因为volatile并不能保证操作的原子性,在读取、写入变量的过程中仍然可能被其他线程打断导致意外结果发生。&,&commentCount&:13,&isNormal&:true,&type&:&answer&,&id&:,&voteupCount&:101}},&answersOffset&:0,&questionId&:,&answerIds&:[],&restAnswerIds&:[],&isLoading&:false,&isDrained&:false,&isModalOpen&:false,&adBanner&:null,&isCommentLoading&:false,&commentsByQuestion&:{},&commentsByAnswer&:{},&relatedLives&:{},&isRegisterPanelOpen&:false,&token&:{&xUDID&:&\&ADDCHxBKBAuPTionNcFY0e5hRVGWfReadf0=|\&&}}' data-config='{&apiAddress&:&/api/v4/&}'>
发表评论:
TA的最新馆藏Hi,欢迎来到中国嵌入式培训第一品牌 - 华清远见嵌入式学院,专注嵌入式工程师培养13年!
全国免费报名电话:400-706-1880
当前位置: >
> volatile在多线程中的应用
volatile在多线程中的应用
时间:作者:华清远见
volatile在词典中的意思是易变的,反复无常的。它在我们的程序设计中常常用到的。volatile是一个关键字,用来修饰一个变量,告诉编译器在编译的时候不要对其进行优化,在操作寄存器和多线程中这种用法是最常见的。
&有这样一个例子:
&#include &stdio.h&
&&&&&&&&&#include &pthread.h&
void my_func();
&&&&&&&&int?
int main()
&&&&&&&&&&&&&&&&pthread_t my_
&&&&&&&&&&&&&&&&int err,k;&
&&&&&&&&&&&&&&&&if ((err = pthread_create(&my_thread,NULL,(void *)my_func,NULL)) & 0)
&&&&&&&&&&&&&&&&perror(&can&#39;t create thread:%s\n&);
&&&&&&&&&&&&&&&&i = 2;
&&&&&&&&&&&&&&&&while(i == 2);
&&&&&&&&&&&&&&&& printf(&main:%d\n&,i);
&&&&&&&&&&&&&&&& while(1);
&&&&&&&&&&&&&&&& return 0;?&
&&&&&&&& }
void my_func()
&&&&&&&&&&&&&&&&sleep(1);
&&&&&&&&&&&&&&&&i = 3;
&&&&&&&&&&&&&&& printf(&my_func:%d\n&,i);
&&&&&&&& }
这个例子本意是想让主程序进入while(i == 2)这个循环,直到线程中将这变量i的值修改后跳出循环,可是结果是
这与想像中的结果完全不一样,是什么原因造成这样的结果呢?查看一下汇编代码,才知道,是编译器将这段代码给优化掉了,汇编代码如下:
.file&& &test.c&
&&&&&&&&.section&&&&& .rodata.str1.1,&aMS&,@progbits,1
&&&&&&&& .string&&&&&&&&&& &my_func:%d\n&
&&&&&&&& .text
&&&&&&&& .p2align 4,,15
.globl my_func
&&&&&&&& .type&&&&& my_func, @function
&&&&&&&& pushl&&&&& %ebp
&&&&&&&& movl&&&&& %esp, %ebp
&&&&&&&& subl $8, %esp
&&&&&&&& movl&&&&& $1, (%esp)
&&&&&&&& call&&&& sleep
&&&&&&&& movl&&&&& $3, 4(%esp)
&&&&&&&& movl&&&&& $.LC0, (%esp)
&&&&&&&& movl&&&&&$3, i
&&&&&&&& call&&& printf
&&&&&&&& leave
&&&&&&&& ret
&&&&&&&& .size my_func, .-my_func
&&&&&&&& .section&&&&& .rodata.str1.1
&&&&&&&& .string&&&&& &can&#39;t create thread:%s\n&
&&&&&&&& .text
&&&&&&&& .p2align 4,,15
.globl main
&&&&&&&& .type&&&&& main, @function
&&&&&&&& main:
&&&&&&&& leal&&&&& 4(%esp), %ecx
&&&&&&&& andl $-16, %esp
&&&&&&&& pushl&&&&& -4(%ecx)
&&&&&&&& pushl&&&&& %ebp
&&&&&&&& movl&&&&& %esp, %ebp
&&&&&&&& pushl&&&&& %ecx
&&&&&&&& subl $36, %esp
&&&&&&&& leal&&&&& -8(%ebp), %eax
&&&&&&&& movl&&&&& $0, 12(%esp)
&&&&&&&& movl&&&&& $my_func, 8(%esp)
&&&&&&&& movl&&&&& $0, 4(%esp)
&&&&&&&& movl&&&&& %eax, (%esp)
&&&&&&&& call&&& pthread_create
&&&&&&&& testl %eax, %eax
&&&&&&&& js&&&&& .L9
&&&&&&&& movl&&&&& $2, i&
&&&&&&&& jmp&&& .L6
&&&&&&&& movl&&&&& $.LC1, (%esp)
&&&&&&&& call&&& perror
&&&&&&&& jmp&&& .L4
&&&&&&&& .size main, .-main
&&&&&&&& .comm&&&&& i,4,4
&&&&&&&& .ident&&&&& &GCC: (GNU) 4.1.3
(prerelease) (Ubuntu 4.1.2-23ubuntu3)&
&&&&&&&& .section&&&&& .note.GNU-stack,&&,@progbits
在定义变量i的时候添加上volatile后:
的结果为:
这个结果显然达到了我们预期的效果,再查看一下他的汇编代码,会看到那个带有条件的循环语句。
.file&&&&& &test.c&
&&&&&&&&.section&&&&& .rodata.str1.1,&aMS&,@progbits,1
&&&&&&&&.string&&&&& &my_func:%d\n&
&&&&&&&& .text
&&&&&&&&.p2align 4,,15
.globl my_func
&&&&&&&& .type&&&&& my_func, @function
&&&&&&&&pushl&&&&& %ebp
&&&&&&&&movl&&&&& %esp, %ebp
&&&&&&&&subl $8, %esp
&&&&&&&&movl&&&&& $1, (%esp)
&&&&&&&&call&& sleep
&&&&&&&&movl&&&&&$3, i
&&&&&&&&movl&&&&& i, %eax
&&&&&&&&movl&&&&& $.LC0, (%esp)
&&&&&&&&movl&&&&& %eax, 4(%esp)
&&&&&&&&call&&printf
&&&&&&&&leave
&&&&&&&&ret
&&&&&&&&.size my_func, .-my_func
&&&&&&&&.section&&&&& .rodata.str1.1
&&&&&&&&.string&&&&& &can&#39;t create thread:%s\n&
&&&&&&&&.string&&&&& &main:%d\n&
&&&&&&&& .text
&&&&&&&&.p2align 4,,15
.globl main
&&&&&&&&.type&&&&& main, @function
&&&&&&&&leal&&&&& 4(%esp), %ecx
&&&&&&&&andl $-16, %esp
&&&&&&&&pushl&&&&& -4(%ecx)
&&&&&&&&pushl&&&&& %ebp
&&&&&&&&movl&&&&& %esp, %ebp
&&&&&&&&pushl&&&&& %ecx
&&&&&&&&subl $36, %esp
&&&&&&&&leal&&&&& -8(%ebp), %eax
&&&&&&&&movl&&&&& $0, 12(%esp)
&&&&&&&&movl&&&&& $my_func, 8(%esp)
&&&&&&&&movl&&&&& $0, 4(%esp)
&&&&&&&&movl&&&&& %eax, (%esp)
&&&&&&&&call&&& pthread_create
&&&&&&&&testl %eax, %eax
&&&&&&&&js&&&&& .L13
&&&&&&&&movl&&&&& $2, i
&&&&&&&&movl&&&&& i, %eax
&&&&&&&&cmpl&&&&& $2, %eax
&&&&&&&&je&&&&&.L6
&&&&&&&&mov&&&&& i, %eax
&&&&&&&&movl&&&&& $.LC2, (%esp)
&&&&&&&&movl&&&&& %eax, 4(%esp)
&&&&&&&&call&&& printf
&&&&&&&&jmp&& .L8
&&&&&&&&movl&&&&& $.LC1, (%esp)
&&&&&&&&call&& perror
&&&&&&&&.p2align 4,,3
&&&&&&&&jmp&& .L4
&&&&&&&&.size main, .-main
&&&&&&&&.comm&&&&& i,4,4
&&&&&&&&.ident&&&&& &GCC: (GNU) 4.1.3
(prerelease) (Ubuntu 4.1.2-23ubuntu3)&
&&&&&&&&.section&&&&& .note.GNU-stack,&&,@progbits
比较红色部分就会看到是什么造成这种差异了!
为什么加上volatile和不加就有这么大的差距的,原因是每次使用变量都去内存中取值,然后通过系统总线传到CPU处理,会增加很大的开销,所以在CPU的cache中位变量啊做了一个副本,通过这个副本来进行赋值。在程序中首先对i 进行复制&i = 2&,然后又将i和2进行比较&i == 2&编译器认为i的值是2,没有变化,认为这个比较没有意义就将其优化掉了,将程序陷入无条件的死循环中,在线程my_func中修改i 的值夜就没有意义了,最终结果就是我么看到了那样了,加上volatile编译器就不会优化了,每次都被迫去内存中去取值,达到了我们预期的结果。
下一篇:没有了
评论列表(网友评论仅供网友表达个人看法,并不表明本站同意其观点或证实其描述)
学院最新动态剖析为什么在多核多线程程序中要慎用volatile关键字?
这篇文章详细分解了为什么在多核时期进行多线程编程时需要慎用volatile关键字。
首要内容有:
1. C/C++中的volatile枢纽字
2. Visual Studio对C/C++中volatile关键字的扩展
3. Java/.NET中的volatile关键字
4. Memory Model(内存模型)
5. Volatile使用建议
1. C/C++中的volatile关键字
1.1 传统用途
C/C++作为系统级说话,它们与硬件的联系是很慎密的。volatile的意思是“易变的”,这个要害字最早就是为了针对那些“异常”的内存操作而筹办的。它的结果是让编译器不要对这个变量的读写操作做任何优化,每次读的时刻都直接去该变量的内存地址中去读,每次写的时辰都直接写到该变量的内存地址中去,即不做任何缓存优化。它经经常使用在需要处置间断的嵌入式系统中,其典型的利用有下面几种:
避免用通用寄存器对内存读写的优化。编译器常做的一种优化就是:把常用变量的频仍读写弄到通用寄存器中,最后不消的时候再存回内存中。但是如果某个内存地址中的值是由片外决议的(例如另一个线程或是另一个设备可能更改它),那就需要volatile关键字了。(感激Kenny教员斧正)
硬件寄存器可能被其他装备改变的情况。例如一个嵌入式板子上的某个寄存器直接与一个测试仪器连在一路,这样在这个寄放器的值随时可能被谁人测试仪器更改。在这种情况下如果把该值设为volatile属性的,那么编译器就会每次都直接从内存中去取这个值的最新值,而不是自作伶俐的把这个值保存在缓存中而致使读不到最新的阿谁被其他设备写入的新值。
同一个物理内存地址M有两个不同的内存地址的环境。例如两个程序同时对统一个物理地址进行读写,那么编译器就不能假定这个地址只会有一个程序接见而做缓存优化,所以程序员在这类情形下也需要把它界说为volatile的。
1.2 多线程法式中的毛病用法
看到这里,良多伴侣天然会想到:恩,那么如果是两个线程需要同时访问一个共享变量,为了让此中两个线程每次都能读到这个变量的最新值,我们就把它定义为volatile的就行了嘛!我想这个就是多线程程序中volatile之所以引发那么多争议的最大缘由。惋惜的是,这个设法是错误的。
举例来讲,想用volatile变量来做同步(例如一个flag)?错!为何?很简单,虽然volatile意味着每次读和写都是直接去内存地址中去操作,可是volatile在C/C++现有尺度中即不克不及包管原子性(Atomicity)也不能保证挨次性(Ordering),所以几近所有试图用volatile来进行多线程同步的方案都是错的
。我之前一篇文章介绍了Sequential
Consistency模型(后面简称SC),它其实就是我们印象中多线程程序应该有的执行顺序。但是,SC最大的问题是性能太低了,因为CPU/编译器完全没有需要严厉按代码划定的递次(program
order)来执行每条指令。学过体系构造的同学应该知道不论是编译器也好CPU也好,他们最善于做的事情就是帮你做乱序优化。在串行时代这些乱序优化对程序员来说都是透明的,封装好了的,你不用关心它们到底给你乱序成啥样了,因为它们会保证优化后的程序的运行结果跟你写程序时预期的了局是如出一辙的。但是进入多核时代之后,CPU和编译器还会继续做那些串行时代的优化,更重要的是这些优化还会打破你多线程程序的SC模型语义,从而使很多线程程序的现实运行后果与我们所等候的运行效果纷歧致!
拿X86来说,它的多核内存模型没有严酷执行SC,即属于weak ordering(或叫relax
ordering?)。它独一答应的乱序优化是可以把对不同地址的load操作提到store之前往(即把store
x-&load y乱序优化成load y -& store
x)。而store x -& store y、load x -& load
y,和store x -& load
y不许可互换执行按次。在X86如许的内存模型下,volatile关键字底子就不能保证对不同volatile变量x和y的store x
-& load y的操作不会被CPU乱序优化成load y -&
而对多线程读写操作的原子性来说,诸如volatile
x=1这样的写操作的原子性实际上是由X86硬件保证的,跟volatile没有任何干系。事实上,volatile基本不能保证对没有内存对齐的变量(或者超越机械字长的变量)的读写操作的原子性。
为了有个更直不雅的理解,我们来看看CPU的乱序优化是如何让volatile在多线程程序中显得如此无力的。下面这个闻名的Dekker算法是想用flag1/2和turn来实现两个线程情况下的临界区互斥访问。这个算法关键就在于对flag1/2和turn的读操作(load)是在其写操作(store)之后的,是以这个多线程算法能保证dekker1和dekker2中对gSharedCounter++的操作是互斥的,即等因而把gSharedCounter++放光临界区里去了。但是,多核X86可能会对这个store-&load操作做乱序优化,例如dekker1中对flag2的读操作可能会被提到对flag1和turn的写操作之前,这样就会终究导致临界区的互斥访问掉效,而gSharedCounter++也会因此产生data
race从而呈现错误的较量争论成效。那么为什么多核CPU会对多线程程序做这样的乱序优化呢?因为从单线程的视角来看flag2和flag1、turn是没有依靠关系的,所以CPU当然可以对他们进行乱序优化以便充实操纵好CPU里面的流水线(想领会更多细节请参考计较机系统布局相关册本)。这样的优化虽然从单线程角度来说没有错,但是它却违背了我们设计这个多线程算法时所期望的那个多线程语义。(想要解决这个bug就需要自己手动添加memory
barrier,或者爽性别去实现这样的算法,而是使用雷同pthread_mutex_lock这样的库函数,后面我会再讲到这点)
当然,对不同的CPU来说他们的内存模型是不同的。好比说,如果这个程序是在单核上以多线程的体式格局执行那么它必定不会犯错,因为单核CPU的内存模型是合适SC的。而在例如PowerPC,ARM之类的架构上运行成绩到底若何就得去翻它们的硬件手册中内存模型是怎样定义的了。
#include &assert.h&
#include &pthread.h&
#include &stdio.h&
#include &stdlib.h&
#undef PRINT_PROGRESS
static volatile int flag1 = 0;
static volatile int flag2 = 0;
static volatile int turn
static volatile int gSharedCounter = 0;
int gLoopC
void dekker1( ) {
flag1 = 1;
while((flag2 ==
1) && (turn == 2)) ;
// Critical section
gSharedCounter++;
// Let the other task run
flag1 = 0;
void dekker2(void) {
flag2 = 1;
while((flag1 ==
1) && (turn == 1)) ;
// critical section
gSharedCounter++;
// leave critical section
flag2 = 0;
// Tasks, as a level of indirection
void *task1(void *arg) {
printf(&Starting task1\n&);
// Do the dekker very many times
#ifdef PRINT_PROGRESS
for(i=0;i&100;i++) {
printf(&[One] at %d%%\n&,i);
for(j=gOnePj&0;j--) {
dekker1();
// Simple basic loop
for(i=gLoopCi&0;i--) {
dekker1();
void *task2(void *arg) {
printf(&Starting task2\n&);
#ifdef PRINT_PROGRESS
for(i=0;i&100;i++) {
printf(&[Two] at %d%%\n&,i);
for(j=gOnePj&0;j--) {
dekker2();
for(i=gLoopCi&0;i--) {
dekker2();
main(int argc, char ** argv)
loopCount = 0;
dekker_thread_1;
dekker_thread_2;
if(argc != 2)
fprintf(stderr, &USAGE: %s &loopcount&\n&, argv[0]);
= atoi(argv[1]);
gLoopCount
gOnePercent = loopCount/100;
expected_sum = 2*loopC
result = pthread_create(&dekker_thread_1, NULL, task1, NULL);
result = pthread_create(&dekker_thread_2, NULL, task2, NULL);
result = pthread_join(dekker_thread_1,&returnCode);
result = pthread_join(dekker_thread_2,&returnCode);
printf(&Both threads terminated\n&);
if( gSharedCounter != expected_sum ) {
printf(&[-] Dekker did not work, sum %d rather than %d.\n&, gSharedCounter, expected_sum);
%d missed updates due to memory consistency races.\n&, (expected_sum-gSharedCounter));
printf(&[+] Dekker worked.\n&);
2. Visual Studio对C/C++中volatile关键字的扩展
固然C/C++中的volatile关头字没有对ordering做任何保证,然则微软从Visual Studio
2005起头就对volatile环节字添加了同步语义(保证ordering),即:对volatile变量的读操作具有acquire语义,对volatile变量的写操作具有release语义。Acquire和Release语义是来自data-race-free模子的概念。为了理解这个acquire语义和release语义有甚么感化,我们来看看MSDN中的一个例子。
// volatile.cpp
// compile with: /EHsc /O2
// Output: Critical Data = 1 Success
#include &iostream&
#include &windows.h&
volatile bool Sentinel =
int CriticalData = 0;
unsigned ThreadFunc1( void* pArguments ) {
while (Sentinel)
// volatile spin lock
// CriticalData load guaranteed after every load of Sentinel
cout && &Critical Data = & && CriticalData &&
ThreadFunc2( void* pArguments ) {
Sleep(2000);
CriticalData++;
// guaranteed to occur before write to Sentinel
Sentinel = // exit critical section
int main() {
HANDLE hThread1, hThread2;
DWORD retC
hThread1 = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)&ThreadFunc1,
NULL, 0, NULL);
hThread2 = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)&ThreadFunc2,
NULL, 0, NULL);
if (hThread1 == NULL || hThread2 == NULL)
cout && &CreateThread failed.& &&
retCode = WaitForSingleObject(hThread1,3000);
CloseHandle(hThread1);
CloseHandle(hThread2);
if (retCode == WAIT_OBJECT_0 && CriticalData == 1 )
cout && &Success& &&
cout && &Failure& &&
例子中的 while (Sentinel) Sleep(0); // volatile spin lock
是对volatile变量的读操作,它具有acquire语义,acquire语义的隐义是当前列程在对sentinel的这个读操作之后的所有的对全局变量的会见都必需在该操作之后执行;同理,例子中的Sentinel
= // exit critical section
是对volatile变量的写操作,它具有release语义,release语义的隐义是当火线程在对sentinel这个写操作之前的所有对全局变量的拜候都必须在该操作之前执行终了。所以ThreadFunc1()读CriticalData时一定已在ThreadFunc2()执行完CriticalData++之后,即CriticalData最后输出的值必定为1。建议各人用纸画一下acquire/release来加深理解。一个对照形象的诠释就是把acquire当成lock,把release当做unlock,它俩构成了一个临界区,所有临界区外面的操作都只能往这个里面移,但是临界区里面的操作都不能往外移,简单吧?
其实这个程序就相当于用volatile变量的acquire和release语义实现了一个临界区,在临界区内部的代码就是
Sleep(2000); CriticalData++;
或者更贴切点也能够当作是一对pthread_cond_wait和pthread_cond_signal。
这个volatile的acquire和release语义是VS自己的扩展,C/C++标准里是没有的,所以一样的代码用gcc编译执行成果就多是错的,因为编译器/CPU可能做背归正确性的乱序优化。Acquire和release语义素质上就是为了保证程序执行时memory
order的正确性。但是,虽然这个VS扩大使得volatile变量能保证ordering,它仍是不能保证对volatile变量读写的原子性。
事实上,如果我们的程序是跑在X86上面的话,内存对齐了的变量的读写的原子性是由硬件保证的,跟volatile没有任何关系。而像volatile
g_nCnt++这样的语句自己就不是原子操作,想要保证这个操作是原子的,就必须使用带LOCK语义的++操作,具体请看我这篇文章。
别的,VS生成的volatile变量的汇编代码是否真的调用了memory
barrier也得看具体的硬件平台,例如x86上就不需要使用memory
barrier也能保证acquire和release语义,因为X86硬件本身就有比力强的memory模型了,但是Itanium上面VS就会生成带memory
barrier的汇编代码。具体可以参考这篇。
但是,虽然VS对volatile关键字插足了acquire/release语义,有一种情况还是会失足,即我们之前看到的dekker算法的例子。
这个其实蛮好理解的,因为读操作的acquire语义不允许在其之后的操作往前移,但是允许在其之前的操作往后移;同理,写操作的release语义允许在其之后的操作往前移,但是不允许在其之前的操作往后移;这样的话对一个volatile变量的读操作(acquire)固然可以放到对另一个volatile变量的写操作(release)之前了!Bug就是这样发生的!下面这个程序大家拿Visual
Studio跑一下就会发现bug了(我试了VS2008和VS2010,都有这个bug)。多线程编程复杂吧?但愿人人还没被弄晕,如果晕了的话也很正常,仔细心细从头再看一遍吧:)
想解决这个Bug也很简单,直接在dekker1和dekker2中对flag1/flag2/turn赋值操作之后都别离插手full
memory barrier就能够了,即保证load必然是在store以后履行便可。具体的我就不胪陈了。
#include &iostream&
#include &windows.h&
static volatile int flag1 = 0;
static volatile int flag2 = 0;
static volatile int turn = 1; // must have &turn&, otherwise the two threads might introduce deadlock at line 13&23 of &while...&
static int gCount = 0;
void dekker1() {
flag1 = 1;
while ((flag2 == 1) && (turn == 2));
// critical section
flag1 = 0;
// leave critical section
void dekker2() {
flag2 = 1;
while ((flag1 == 1) && (turn == 1));
// critical setion
flag2 = 0;
// leave critical section
unsigned ThreadFunc1( void* pArguments ) {
//cout && &Starting Thread 1& &&
for (i=0;i&1000000;i++) {
dekker1();
ThreadFunc2( void* pArguments ) {
//cout && &Starting Thread 2& &&
for (i=0;i&1000000;i++) {
dekker2();
int main() {
HANDLE hThread1, hThread2;
//DWORD retC
hThread1 = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)&ThreadFunc1,
NULL, 0, NULL);
hThread2 = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)&ThreadFunc2,
NULL, 0, NULL);
if (hThread1 == NULL || hThread2 == NULL) {
cout && &CreateThread failed.& &&
WaitForSingleObject(hThread1,INFINITE);
WaitForSingleObject(hThread2,INFINITE);
cout && gCount &&
if (gCount == 2000000)
cout && &Success& &&
cout && &Fail& &&
3. Java/.NET中的volatile关键字
3.1 多线程语义
Java和.NET划分有JVM和CLR这样的虚拟机,保证多线程的语义就轻易多了。说简单点,Java和.NET中的volatile关键字也是限制虚拟机做优化,都具有acquire和release语义,而且由虚拟机直接保证了对volatile变量读写操作的原子性。(volatile只保证可见性,不保证原子性。java中,对volatile润饰的long和double的读写就不是原子的
(/docs/books/jvms/second_edition/html
/Threads.doc.html#22244),除此以外的根基类型和援用类型都是原子的。烦忙
多谢liuchangit指正)
这里需要留意的一点是,Java和.NET里面的volatile没有对应于我们最入手下手提到的C/C++中对“异常操作”用volatile润色的传统用法。原因很简单,Java和.NET的虚拟机对安全性的要求比C/C++高多了,它们才不允许不安全的“异常”访问存在呢。
而且像JVM/.NET这样的程序可移植性都十分好。虽然而今C++1x正在把多线程模型添加到标准中去,但是因为C++本身的性质导致它的硬件平台依赖性很高,可移植性不是分外好,所以在移植C/C++多线程程序时理解硬件平台的内存模型是特别很是主要的一件工作,它直接决意你这个程序是否会正确执行。
至于Java和.NET中是否也存在近似VS
2005那样的bug我没时候去测试,事理其实是不异的,真有需要的同窗自己应当能测出来。仿佛这篇InfoQ的文章中显示Java运行这个dekker算法没有问题,因为JVM给它添加了mfence。另一个污名昭著的例子就应该是Double-Checked
Locking了。
3.2 volatile int与AtomicInteger区分
Java和.NET中这二者还是有些区此外,主要就是后者提供了类似incrementAndGet()这样的方法可以直接调用(保证了原子性),而如果是volatile
x进行++操作则不是原子的。increaseAndGet()的实现调用了类似CAS这样的原子指令,所以能保证原子性,同时又不会像使用synchronized关键字一样损失许多性能,用来做全局计数器极度适合。
4. Memory Model(内存模型)
说了这么多,照旧顺带介绍一下Memory
Model吧。就像前面说的,CPU硬件有它自己的内存模型,分歧的编程语言也有它本身的内存模型。假如用一句话来介绍什么是内存模型,我会说它就是程序员,编程语言和硬件之间的一个契约,它保证了同享的内存地址里的值在需要的时候是可见的。下次我会专门具体写一篇关于它的内容。它最年夜的作用是获得可编程性与性能优化之间的一个均衡。
5. volatile使用建议
总的来说,volatile关键字有两种用途:一个是ISO
C/C++中用来处置惩罚“异常”内存行动(此用途只保证不让编译器做任何优化,对多核CPU是否会进行乱序优化没有任何束缚力),另一种是在Java/.NET(包罗Visual
Studio添加的扩展)顶用来实现高性能并行算法(此种用途通过使用memory
barrier保证了CPU/编译器的ordering,以及经由过程JVM或者CLR保证了对该volatile变量读写操作的原子性)。
一句话,volatile对多线程编程是很是危险的,使用的时候万万要谨慎你的代码在多核上究竟是不是按你所想的体例执行的,出格是对此刻临时还没有引入内存模型的C/C++程序更是如斯。安全起见,大师照样用Pthreads,Java.util.concurrent,TBB等并行库供给的lock/spinlock,conditional
variable, barrier, Atomic
Variable之类的同步方式来干活的好,由于它们的内部实现都挪用了响应的memory barrier来保证memory
ordering,你只要保证你的多线程程序没有data
race,那么它们就可以帮你保证你的程序是准确的(是的,Pthreads库也是有它自己的内存模型的,只不外它的内存模型还些错误谬误,所以把多线程内存模型直接集成到C/C++中是更好的法子,也是未来的趋向,但是C++1x中将不会像Java/.NET一样给volatile关键字添加acquire和release语义,而是转而供应另外一种具有同步语义的atomic
variables,此为后话)。若是你想实现更高机能的lock
free算法,或是利用volatile来进行同步,那末你就需要先把CPU和编程语言的memory
model弄清晰,然后再时刻注重Atomicity和Ordering是不是被保证了。
(注意,用没有acquire/release语义的volatile变量来进行同步是错误的,但是你依然可以在C/C++中用volatile来修饰一个不是用来做同步(例如一个event
flag)而只是被不同线程读写的共享变量,只不过它的新值什么时候能被另一个线程读到是没有保证的,需要你自己做相应的处理)
Herb Sutter 在他的那篇volatile vs.
volatile中对这两种用法做了很仔细的辨别,我把个中两张表格链接贴过来供大家参考:
volatile的两种用途
volatile两种用处的异同
最后附上《Java Concurrency in
Practice》3.1.4节中对Java语言的volatile关键字的使用建议(不要被英语吓到,这些内容确切对你有效,并且还能趁便帮练练英语,哈哈):
So from a memory visibility perspective, writing a volatile
variable is like exiting a synchronized block and reading a
volatile variable is like entering a synchronized block. However,
we do not recommend relying too heavily on volatile variables for
code that relies on volatile variables for visibility
of arbitrary state is more fragile and harder to understand than
code that uses locking.
Use volatile variables only when they simplify implementing and
verifying your sy avoid using volatile
variables when veryfing correctness would require subtle reasoning
about visibility. Good uses of volatile variables include ensuring
the visibility of their own state, that of the object they refer
to, or indicating that an important lifecycle event (such as
initialization or shutdown) has occurred.
Locking can guarantee both visi volatile
variables can only guarantee visibility.
You can use volatile variables only when all the following
criteria are met:
(1) Writes to the variable do not depend on its current value, or
you can ensure that only a single thread ever updates the
(2) The variable does not participate in invariants with other
(3) Locking is not required for any other reason while the variable
is being accessed.
1.《Java Concurrency in Practice》3.1.4节
2.volatile vs. volatile(Herb Sutter对volatile的论述,必看)
3.The “Double-Checked Locking is Broken” Declaration
4.Threading in C#
5.Volatile: Almost Useless for Multi-Threaded Programming
6.Memory Ordering in Modern Microprocessors
7.Memory Ordering @ Wikipedia
8.内存樊篱什么的
9.The memory model of x86
10.VC 下 volatile 变量可否成立 Memory Barrier 或并发锁
11.Sayonara volatile(Concurrent Programming on Windows作者的文章
跟我概念几乎一致)
12.Java 理论与实践: 正确使用 Volatile 变量
13.Java中的Volatile关键字
冠诚, IBM中国研究院, 研究员
任何与多核、并行、多线程有关的话题都可以找我聊聊:-) 我的邮箱是chenguancheng
Blog豆瓣TwitterLinkedInBuzz新浪微博
向文章付费请作者吃饭
04/09/2011 --《程序员的自我涵养》中关于加锁不能保证线程平安的一个错误
11/13/2010 --多线程程序常见Bug解析(上)
04/15/2010 --多线程程序中操作的原子性
01/31/2010 --Pthreads并行编程之spin lock与mutex性能对照阐发
10/02/2011 --并行编程中的“锁”困难
已投稿到:
以上网友发言只代表其个人观点,不代表新浪网的观点或立场。

我要回帖

更多关于 linux下多线程编程 的文章

 

随机推荐