none
VC8中volatile无法起到预期的memory barrier作用 RRS feed

  • 问题

  • 想用volatile变量做个应用层的自旋锁设施。

    关于volatile参考:http://msdn.microsoft.com/en-us/library/12a04hfd(VS.80).aspx

    我先做了一个volatile全局变量的测试,代码如下:

     

    typedef struct _ThreadParam
    {
      UINT nRound;
    }ThreadParam;
    
    volatile UINT g_nCnt;
    
    int _tmain()
    {
      UINT nRnd1, nRnd2, nTestRnd;
      const UINT RANGE_MIN = 10;
      const UINT RANGE_MAX = 100;
    
      srand((UINT)time(NULL));
    
      nTestRnd = 0;
      while (TRUE)
      {
        g_nCnt = 0;
        nRnd1 = (UINT)(((double)rand() / (double)RAND_MAX) * RANGE_MAX) + RANGE_MIN;
        nRnd2 = (UINT)(((double)rand() / (double)RAND_MAX) * RANGE_MAX) + RANGE_MIN;
    
        nTestRnd++;
        TestConcurrentAccess(nRnd1, nRnd2);
    
        _tprintf(_T("%d. round 1: %d, round 2: %d\n"), nTestRnd, nRnd1, nRnd2);
        _tprintf(_T("global counter: %d\n"), g_nCnt);
        if (nRnd1 + nRnd2 != g_nCnt)
          break;
      }
    
      return 0;
    }
    
    DWORD TestConcurrentAccess(UINT nRnd1, UINT nRnd2)
    {
      DWORD dwTid;
      HANDLE hThd, hThd2;
    
      ThreadParam p1, p2;
    
      p1.nRound = nRnd1;
      p2.nRound = nRnd2;
    
      hThd = CreateThread(NULL, 0, ThreadTest, &p1, 0, &dwTid);
      hThd2 = CreateThread(NULL, 0, ThreadTest, &p2, 0, &dwTid);
    
      WaitForSingleObject(hThd, INFINITE);
      WaitForSingleObject(hThd2, INFINITE);
    
      return 0;
    }
    
    DWORD WINAPI ThreadTest(LPVOID pParam)
    {
      _ASSERT(pParam != NULL);
    
      ThreadParam* pThdParam = (ThreadParam*)pParam;
    
      for (UINT i = 0; i < pThdParam->nRound; i++)
      {
        Sleep(5);
        g_nCnt++;
      }
    
      return 0;
    }
    
    

    如果上面全局变量 g_nCnt 被声明为 UINT g_nCnt; 则理论上因为线程调度的原因,g_nCnt++ 操作有被打断的可能,导致 _tmain() 测试中的 nRnd1 + nRnd2 != g_nCnt 条件满足,从而退出程序。实际测试中确实是这样,测试了几次,每次 TestConcurrentAccess() 用例没跑过 50 轮,就会达到这个错误条件。而且每次都是 g_nCnt 比 nRnd1 + nRnd2 小 1。

    我觉得用 volatile 修饰 g_nCnt 后,编译器在 g_nCnt 上建立了存储壁障代码,应该能解决 g_nCnt++ 被打断的问题,可是实测后发现,volatile UINT g_nCnt; 依然会导致到达 nRnd1 + nRnd2 != g_nCnt 条件( 我的实测中,TestConcurrentAccess() 用例不超过 100 轮,就会达到这个条件)。

    大家可以试试上面的代码,期望的情况是永远都执行下去,只能用 Ctrl+C 中断。

    请教一下各位有谁能解释这个问题。

    PS:

    编译环境 VC8,关键的编译选项 /O2 /EHsc /MD,平台 ntkrpamp.exe(带PAE的MP版内核) Core 2 Duo

     

    2010年7月19日 15:37

答案

全部回复

  • 在您说的这种情况下, 需要使用::InterLockIncrement(&g_nCnt); 的方式实现g_nCnt 变量的累加。 这个是带锁的方式。
    2010年7月19日 23:44
    版主
  •  
     
    • 已标记为答案 breaker1024 2010年7月21日 13:52
    • 已编辑 ID已删 2010年8月16日 4:37
    2010年7月20日 1:13
  • 多谢 @江写生,你讲的很透彻。

    后来看了一些资料,又实验了下,总结了下,要实现 memory barrier 实际涉及的问题有以下这些。

    volatile 修饰的变量,只保障两个事情:

    1. 防止运行时,CPU 做 cache。
    2. 防止编译时,编译器对 volatile 变量的操作指令及其附近的指令,做 reorder。

    但是 volatile 不保障对其修饰的变量的操作指令是原子的,这意味着不提供 MP 之间的指令并发同步。

    MP 和 UP 上的原子操作实现考虑的问题不同,UP 上只考虑调度切换,而要实现 MP 上的原子写操作(同步),还需要锁 bus:

    3. x86 上用 LOCK 前缀指令指示,在后跟指令的执行时间内发 LOCK# 锁住 bus。

    在 P4 之后的 CPU 上,有 LOCK 前缀不一定发 LOCK#,但对上面的 non-cached 的 volatile 数据一定是有 LOCK# 的。

    1、2、3 点共同作用,使得 MP 上写操作是原子的。所以看看 intrin.h 中的 _InterlockedIncrement() 声明的参数也是 volatile* 的:

    long __cdecl _InterlockedIncrement(long volatile*);

    ----------

    以下是我用 OllyDbg 调试的代码,供参考:

    release: /O2 /EHsc /MD
    debug: /Od /EHsc /MDd

    ----------

    1. 先看出错代码的(只用到 volatile 变量):

    volatile UINT g_nCnt;
    
    __asm int 3;
    g_nCnt++;
    __asm nop;
    

    (1). debug 编译为:

    INT3
    MOV EAX,DWORD PTR DS:[417188]
    ADD EAX,1
    MOV DWORD PTR DS:[417188],EAX
    NOP

    (2). release 编译为:

    INT3
    ADD DWORD PTR DS:[403378],1
    NOP

    和 @江写生 说的一条 INC 指令类似,只不过变成了一条 Opcode=8305 的 ADD,大同小异。

    在实测中也会达到 nRnd1 + nRnd2 != g_nCnt 的条件,就说明这一条指令 ADD DWORD PTR DS:[403378],1 在 Core 2 Duo 的 MP 环境下不是原子的,当两个核同时执行此指令时就会出问题,所以要锁 bus。

    PS.

    实测中加不加 volatile 的 UINT g_nCnt,都生成上面执行指令,我猜 volatile 的 non-cached 特性可能是使用数据段区的标识位来实现,想到有个预处理指示 #pragma section("section", nocache)。

    ----------

    2. 再看用 _InterlockedIncrement() 的:

    volatile UINT g_nCnt;
    
    __asm int 3;
    _InterlockedIncrement((long*)&g_nCnt);
    __asm nop;
    

    release、debug 均编译为类似:

    INT3
    MOV EAX,Volatile.00403378
    MOV ECX,1
    LOCK XADD DWORD PTR DS:[EAX],ECX
    NOP

    PS.

    实测中,用 volatile UINT g_nCnt + _InterlockedIncrement() 方法,500 轮测试中没出现过 nRnd1 + nRnd2 != g_nCnt 的问题。

    而用 UINT g_nCnt + _InterlockedIncrement() 方法,500 轮测试也没有出现问题,难道仅靠到 _InterlockedIncrement() 的参数 long volatile * 的类型转换就起到了 non-cached 的作用?还是出错的时机未到。
    2010年7月20日 15:57
  •  
     
    • 已标记为答案 breaker1024 2010年7月21日 13:51
    • 已编辑 ID已删 2010年8月16日 4:38
    2010年7月21日 10:03
  • 多谢 @江写生 , 思路很清晰,例子举得很好啊,受益匪浅。
    2010年7月21日 13:46