【问题标题】:G++ 4.6 -std=gnu++0x: Static Local Variable Constructor Call Timing and Thread SafetyG++ 4.6 -std=gnu++0x:静态局部变量构造函数调用时序和线程安全
【发布时间】:2012-03-02 12:53:39
【问题描述】:
void a() { ... }
void b() { ... }

struct X
{
    X() { b(); }
};

void f()
{
    a();
    static X x;
    ...
}

假设在进入 main 之后,从各种线程(可能竞争)多次调用 f。 (当然,对 a 和 b 的唯一调用是上面看到的)

上述代码在-std=gnu++0x模式下使用gcc g++ 4.6编译时:

第一季度。是否保证 a() 至少会被调用一次并在调用 b() 之前返回?也就是说,在第一次调用 f() 时,x 的构造函数是否会同时调用一个自动持续时间局部变量(非静态)(而不是在全局静态初始化时)?

第二季度。是否保证 b() 将被调用一次?即使两个线程第一次在不同的内核上同时执行 f ?如果是,GCC 生成的代码通过哪种具体机制提供同步? 编辑:另外,调用 f() 的线程之一能否在 X 的构造函数返回之前获得对 x 的访问权?

更新:我正在尝试编译一个示例并反编译以调查机制...

test.cpp:

struct X;

void ext1(int x);
void ext2(X& x);

void a() { ext1(1); }
void b() { ext1(2); }

struct X
{
    X() { b(); }
};

void f()
{
    a();
    static X x;
    ext2(x);
}

然后:

$ g++ -std=gnu++0x -c -o test.o ./test.cpp
$ objdump -d test.o -M intel > test.dump

test.dump:

test.o:     file format elf64-x86-64


Disassembly of section .text:

0000000000000000 <_Z1av>:
   0:   55                      push   rbp
   1:   48 89 e5                mov    rbp,rsp
   4:   bf 01 00 00 00          mov    edi,0x1
   9:   e8 00 00 00 00          call   e <_Z1av+0xe>
   e:   5d                      pop    rbp
   f:   c3                      ret    

0000000000000010 <_Z1bv>:
  10:   55                      push   rbp
  11:   48 89 e5                mov    rbp,rsp
  14:   bf 02 00 00 00          mov    edi,0x2
  19:   e8 00 00 00 00          call   1e <_Z1bv+0xe>
  1e:   5d                      pop    rbp
  1f:   c3                      ret    

0000000000000020 <_Z1fv>:
  20:   55                      push   rbp
  21:   48 89 e5                mov    rbp,rsp
  24:   41 54                   push   r12
  26:   53                      push   rbx
  27:   e8 00 00 00 00          call   2c <_Z1fv+0xc>
  2c:   b8 00 00 00 00          mov    eax,0x0
  31:   0f b6 00                movzx  eax,BYTE PTR [rax]
  34:   84 c0                   test   al,al
  36:   75 2d                   jne    65 <_Z1fv+0x45>
  38:   bf 00 00 00 00          mov    edi,0x0
  3d:   e8 00 00 00 00          call   42 <_Z1fv+0x22>
  42:   85 c0                   test   eax,eax
  44:   0f 95 c0                setne  al
  47:   84 c0                   test   al,al
  49:   74 1a                   je     65 <_Z1fv+0x45>
  4b:   41 bc 00 00 00 00       mov    r12d,0x0
  51:   bf 00 00 00 00          mov    edi,0x0
  56:   e8 00 00 00 00          call   5b <_Z1fv+0x3b>
  5b:   bf 00 00 00 00          mov    edi,0x0
  60:   e8 00 00 00 00          call   65 <_Z1fv+0x45>
  65:   bf 00 00 00 00          mov    edi,0x0
  6a:   e8 00 00 00 00          call   6f <_Z1fv+0x4f>
  6f:   5b                      pop    rbx
  70:   41 5c                   pop    r12
  72:   5d                      pop    rbp
  73:   c3                      ret    
  74:   48 89 c3                mov    rbx,rax
  77:   45 84 e4                test   r12b,r12b
  7a:   75 0a                   jne    86 <_Z1fv+0x66>
  7c:   bf 00 00 00 00          mov    edi,0x0
  81:   e8 00 00 00 00          call   86 <_Z1fv+0x66>
  86:   48 89 d8                mov    rax,rbx
  89:   48 89 c7                mov    rdi,rax
  8c:   e8 00 00 00 00          call   91 <_Z1fv+0x71>

Disassembly of section .text._ZN1XC2Ev:

0000000000000000 <_ZN1XC1Ev>:
   0:   55                      push   rbp
   1:   48 89 e5                mov    rbp,rsp
   4:   48 83 ec 10             sub    rsp,0x10
   8:   48 89 7d f8             mov    QWORD PTR [rbp-0x8],rdi
   c:   e8 00 00 00 00          call   11 <_ZN1XC1Ev+0x11>
  11:   c9                      leave  
  12:   c3                      ret    

我没有看到同步机制?还是在链接时添加的?

Update2:好的,当我链接它时,我可以看到它...

400973: 84 c0                   test   %al,%al
400975: 75 2d                   jne    4009a4 <_Z1fv+0x45>
400977: bf 98 20 40 00          mov    $0x402098,%edi
40097c: e8 1f fe ff ff          callq  4007a0 <__cxa_guard_acquire@plt>
400981: 85 c0                   test   %eax,%eax
400983: 0f 95 c0                setne  %al
400986: 84 c0                   test   %al,%al
400988: 74 1a                   je     4009a4 <_Z1fv+0x45>
40098a: 41 bc 00 00 00 00       mov    $0x0,%r12d
400990: bf a0 20 40 00          mov    $0x4020a0,%edi
400995: e8 a6 00 00 00          callq  400a40 <_ZN1XC1Ev>
40099a: bf 98 20 40 00          mov    $0x402098,%edi
40099f: e8 0c fe ff ff          callq  4007b0 <__cxa_guard_release@plt>
4009a4: bf a0 20 40 00          mov    $0x4020a0,%edi
4009a9: e8 72 ff ff ff          callq  400920 <_Z4ext2R1X>
4009ae: 5b                      pop    %rbx
4009af: 41 5c                   pop    %r12
4009b1: 5d                      pop    %rbp

它用 __cxa_guard_acquire__cxa_guard_release 围绕着它,不管他们做什么。

【问题讨论】:

  • Q2 :只保证一次。 C++11 知道线程并且在标准中指定。至于机制,不知道,但它与互斥锁没有太大区别;)。
  • Q2:保证(正如其他人已经指出的那样)。 Q1:我不会打赌。声明一个静态变量几乎不是程序流程的一部分,优化器是臭名昭著的代码混洗器。无论如何,我不想维护依赖于这种微妙之处的代码。
  • @stefaanv:不正确。标准和实施都保证了订单。

标签: c++ gcc g++ c++11


【解决方案1】:

第一季度。是的。根据C++11,6.7/4:

这样的变量在控件第一次通过其声明时被初始化

所以它会在第一次调用a() 后被初始化。

第二季度。在 GCC 和任何支持 C++11 线程模型的编译器下:是的,局部静态变量的初始化是线程安全的。其他编译器可能不会提供这种保证。确切的机制是一个实现细节。我相信 GCC 使用原子标志来指示它是否已初始化,并在未设置标志时使用互斥体来保护初始化,但我可能是错的。当然,this thread 暗示它最初是这样实现的。

更新:您的代码确实包含初始化代码。把它链接起来可以看得更清楚,然后反汇编程序,这样就可以看到调用了哪些函数。我还使用objdump -SC 来交错源代码和对 C++ 名称进行分解。它使用内部锁定函数__cxa_guard_acquire__cxa_guard_release,以确保只有一个线程执行初始化代码。

  #void f()
  #{
  400724: push   rbp
  400725: mov    rbp,rsp
  400728: push   r13
  40072a: push   r12
  40072c: push   rbx
  40072d: sub    rsp,0x8

  # a();
  400731: call   400704 <a()>

  # static X x;
  # if (!guard) {
  400736: mov    eax,0x601050
  40073b: movzx  eax,BYTE PTR [rax]
  40073e: test   al,al
  400740: jne    400792 <f()+0x6e>

  #     if (__cxa_guard_acquire(&guard)) {
  400742: mov    edi,0x601050
  400747: call   4005c0 <__cxa_guard_acquire@plt>  
  40074c: test   eax,eax
  40074e: setne  al
  400751: test   al,al
  400753: je     400792 <f()+0x6e>

  #         // initialise x
  400755: mov    ebx,0x0
  40075a: mov    edi,0x601058
  40075f: call   4007b2 <X::X()>

  #         __cxa_guard_release(&guard);
  400764: mov    edi,0x601050
  400769: call   4005e0 <__cxa_guard_release@plt>

  #     } else {
  40076e: jmp    400792 <f()+0x6e>

  #         // already initialised
  400770: mov    r12d,edx
  400773: mov    r13,rax
  400776: test   bl,bl
  400778: jne    400784 <f()+0x60>
  40077a: mov    edi,0x601050
  40077f: call   4005f0 <__cxa_guard_abort@plt>
  400784: mov    rax,r13
  400787: movsxd rdx,r12d
  40078a: mov    rdi,rax
  40078d: 400610 <_Unwind_Resume@plt>

  #     }
  # }
  # ext2(x);
  400792: mov    edi,0x601058
  400797: call   4007d1 <_Z4ext2R1X>
  #}

【讨论】:

    【解决方案2】:

    据我所知,可以保证 b 只被调用一次。但是,不能保证初始化是线程安全的,这意味着另一个线程可能会使用半/未初始化的 x。 (这有点好笑,因为静态互斥体在这种情况下基本上没用。)

    【讨论】:

    • 我认为这不是使用给定命令行参数启用的 C++11 中的行为。不过在旧标准中它是正确的。
    • C++11 需要线程安全的初始化,GCC 已经默认提供了相当长一段时间。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-12-24
    • 1970-01-01
    • 2010-09-05
    • 1970-01-01
    相关资源
    最近更新 更多