Win32多线程之volatile

来源:互联网 发布:apache开源社区 编辑:程序博客网 时间:2024/05/16 10:03

   我相信你一定遇到这样的问题:你把某人的名字和电话号码写到你的通讯录中,数个月之后企图打电话给这个人,却发现资料已经过期了。同样的情况也可能发生在编译器为你产生的程序代码中。

   编译器最优化的结果是,设法把常用的数据放在CPU的内部寄存器中,这些寄存器就像你的通讯录一样,数据从寄存器中读出,远比从内存中读出快得多,就好像使你从你的通讯录中读数据,远比从大电话簿中读数据要快得多,当然,如果另一个线程改变了内存的变量值,那么此变量在寄存器中的拷贝值就算是“过期”了。

  在一个单线程中,这种情况不可能发生,编译器可以分析你的程序的每一个操作,然后确保数据在适当时候会被重新载入。然而在一个多线程中就不可能知道其他线程在做什么,所以编译器一定不能够允许让一个共享变量的值拷贝到寄存器中。

   C和C++有一个鲜为人知的关键字,教导编译器如何在一个variable-by-variable 的基础上采取行动,这个关键字是volatile。这个关键字告诉编译器不要持有变量的临时性拷贝,它可以适用于基础类型,如int或long,也适用于一整个C结构或C++类。后面这种情况下,结构或类的所有成员都会被视为volatile。

  使用volatile并不会否定critical sections或mutexes的需要。

例如你说:

 a = a + 3


还是会有一小段时间,a会被放在一个寄存器中,因为算数运算只能够在寄存器中进行。一般而言,volatile关键字适用于行与行之间,而不是放在行内。

  让我们看一个非常简单的函数,观察编译器制造出来的汇编语言中的瑕疵,并看看volatile如何修正这个瑕疵。这个范例函数就是一个busy loo【

,虽然不建议写busy loop,但是它却是解释这里的观念的一个最好的例子。本例之中,WaitForKey()等待一个字符的到来:


void   WaitForKey(char* pch)

{

  while ( *pch == 0 )

       ;  

}

当你把最优化选项都关闭后,编译这个程序,获得以下结果,进入点和退出点已经被我移除(为了让程序代码清爽一些),粗体字代表C源代码。


;                          while(*pch == 0)

$L27:

           ;     Load the address stored in pch

           mov eax, DWORD PTR  _pch$[ebp]

           ;     Load the character into the EAX register

            movsx     eax,BYTE   PTR  _pch$[eax]

           ;Compare the value to zero

             test  eax,  eax 

             ;  If   not   zero,  exit   loop

             jne    $L28

;      ;

            jmp      $L27

$L28:

  ; }                


  这个未曾优化的函数代码不断地载入适当的地址,载入地址中的内容,测试其结果,慢,但是准确,此版本在多线程程序上没有问题。

  现在我们看看最优化带来什么影响:

; {

           ; Load the address stored in pch

            mov eax,  DWORD  PTR  _pch$[esp - 4]

             ;  Load the character into the AL register

           movsx  al,  BYTE  PTR  [eax]

;    while  (*pch  == 0)

$L84:

            ; Compare the value in the AL  register to zero

            test  al, al

            ;  If  still  zero,  try again

             je        SHORT   $L84

;           ;

;  }


    短多了,最优化果然有用。但是请注意,编译器把MOV指令放到循环之外,这个操作称为 loop-invariant removal。这个在单线程程序中应该是一个很好地最优化,但是在多线程程序中,如果另一个线程改变了数值,则循环永远不会结束。被测试的值永远被放在寄存器中,很明显那是一只“臭虫”。

  解决方法就是重写WaitForKey(),把参数pch声明为 volatile:


   void  WaitForKey (volatile char* pch)

{

  while (* pch == 0 )

  ;

}

这项改变对于非最优化的版本没有影响,但请看看最优化后的结果:

 ; {

              ;  Load the address stored in pch

              mov  eax,  DWORD  PTR  _pch$[esp-4 ]

;       while  (*pch  == 0)

$L84:

          ; Directly  compare the value to zero

          cmp   BYTE  PTR   [eax],  0

          ; If  still zero, try again

          je          SHORT  $L84

;                  ;

;  }


这个版本几乎完美,地址不会改变,所以地址声明被移到循环之外,地址内容是volatile,所以每次循环之中它不断地被重新检查。

   精细地说,把一个const volatile 变量传给函数作为参数是合法的,如此的声明意味着函数不能够改变变量的值,但是变量的值却可以被另一个线程在任何时间改变掉。

  const 和 volatile 都是ANSI的标准关键字,所有的C/C++编译器都应该有支持。