【问题标题】:In C++, are static initializations of primitive types to constant values thread-safe?在 C++ 中,将原始类型静态初始化为常量值是线程安全的吗?
【发布时间】:2010-02-01 22:20:50
【问题描述】:

即,即使在多线程环境中,以下内容是否也能正确执行?

int dostuff(void) {
    static int somevalue = 12345;
    return somevalue;
}

或者是否有可能多个线程调用它,并且一个调用返回在执行开始之前&somevalue 处的任何垃圾?

【问题讨论】:

  • static const 不是一个选项?
  • constness 在这里不起作用,因为 int 稍后会被修改(同时持有一个已知有效的互斥锁。)我的直觉是任何理智的编译器都会至少零初始化静态开始执行之前函数范围内的整数(在这种情况下,这对我来说已经足够了。)不过,这种事情很容易出错。
  • 技术上没有。但是 gcc 有一个明确的补丁来保证它可以在多线程环境中工作。
  • 确实——不幸的是,我的代码也需要为 VC++ 9.0 编译,这意味着我必须编写避免 GCC 特定内容的代码。在这种情况下,代码最终的功能类似于 pthread_once(现在我想起来,在 C++ 中会遇到同样的问题),但由于 VC++ 要求不能直接使用 pthread_once。
  • @Martin: ... 除非使用“-fno-thread-safe-statics”命令行选项明确禁用它(参见gcc.gnu.org/onlinedocs/gcc/C_002b_002b-Dialect-Options.html)。 +1,无论如何。

标签: c++ static multithreading constants


【解决方案1】:

标准的第 6.7 节有这样的说法:

所有本地的零初始化 具有静态存储持续时间的对象 在任何其他之前执行 初始化发生。本地人 具有静态存储的 POD 类型的对象 持续时间初始化为 常量表达式被初始化 在第一次进入它的块之前。一个 允许执行 其他本地的早期初始化 具有静态存储持续时间的对象 在相同的条件下,一个 允许实施 静态初始化一个对象 命名空间中的静态存储持续时间 范围。 否则这样的对象是 初始化第一次控制 通过其声明;这样的 一个对象被认为是初始化的 在其完成后 初始化。 如果初始化 通过抛出异常退出, 初始化没有完成,所以 下次会再试 控制进入声明。 如果 控制重新进入声明 (递归地)当对象是 被初始化,行为是 未定义。

因此,如果它是 POD 类型,那么看起来初始化发生在启动时,然后才能启动新线程。对于非 POD 类型,它更复杂,标准说行为是未定义的(除非在其他地方它说明了初始化期间的线程安全)。

我碰巧知道,当初始化一个非 POD 对象时,GCC 会抓取一个互斥锁以防止它被初始化两次(我知道这一点是因为我曾经因为意外地递归初始化一个静态对象而导致程序死锁)。

很遗憾,我无法告诉您这是其他编译器的情况,还是标准中其他地方的规定。

【讨论】:

  • “一个 POD 类型的本地对象,其静态存储持续时间用常量表达式初始化,在其块首次进入之前被初始化。”即不在启动时。
  • 对不起,我没有正确解释我。由于我找不到任何进一步的说明,我必须假设即使是 POD 初始化也允许推迟到第一次使用该函数,使其不是线程安全的。
【解决方案2】:

是的,它是完全安全的(在大多数编译器上)。我建议在你的特定编译器上设置一个断点并查看分配是如何完成的。我无法告诉你有多少次违反了“标准”。

如果您从函数或方法调用的结果中分配本地静态,那么您可能会处理竞争条件。对原始类型的常量分配通常会得到优化。

在 OS X 10.6.2 的 g++ 上,这是为您的函数生成的机器代码:

push   rbp
mov    rbp,rsp
lea    rax,[rip+0x2067]        # 0x100003170 <_ZZ7dostuffvE9somevalue>
mov    eax,DWORD PTR [rax]
leave  
ret

如您所见,没有分配。编译器在构建时已经烘焙了原语。

【讨论】:

  • 另外,如果 someValue 实际上在函数中被修改,我想看看 ASM - 我认为 GCC 在这里做了一些优化。我感觉生成的代码更像:stackoverflow.com/questions/2180501/…
  • 当我增加值时,它看起来与上面完全相同,除了增量 ASM。改变值会引入竞争,但不是因为初始化,它仍然不存在。
【解决方案3】:

来自 C++ 标准,第 6.7 节:

具有静态存储持续时间的 POD 类型 (3.9) 本地对象 用常量表达式初始化 在其块被初始化之前被初始化 首次进入。

这意味着函数级静态对象必须在第一次进入函数时初始化,而不一定是在整个过程初始化时。此时,可能有多个线程正在运行。

【讨论】:

  • 啊,在标准中找不到。啊!同一节说:“所有具有静态存储持续时间(3.7.1)的本地对象的零初始化(8.5)在任何其他初始化发生之前执行。”那么,如果初始值为 0,即使在多线程环境中,0 的值也应该(根据标准)有效,是吗?
  • 确实是这么说的。
  • @Jonathan BTW,我不会太快接受这一点 - 我敢肯定这句话的含义会有分歧:-)
  • 如果存储是在第一次进入其块之前初始化的,那么第一次调用函数时它是如何被初始化的?
  • @R Samuel Klatchko 我说它必须在第一次调用函数时进行初始化。
【解决方案4】:

因为 somevalue 初始化器不需要构造函数调用,所以可以正常工作(somevalue 将在构建时初始化)。

现在,如果您正在初始化一个需要构造函数的值:

void whatever()
{
    static std::string value("bad");

    ...
}

然后你可能会遇到多个线程的麻烦。在内部,这将变成类似:

void whatever()
{
    static bool value_initialized = false;
    static string_struct value;

    if (!initialized)
    {
        construct_string(&value, "bad");
        value_initialized = false;
    }

    ....
 }

在存在多个线程的情况下,您会遇到各种问题,包括竞争条件和内存可见性)。

【讨论】:

    【解决方案5】:

    根据我的经验,在文件范围内定义的静态变量的行为不同于在函数中定义的静态变量

    文件作用域一在所有线程开始之前被安全地初始化,函数作用域一不是。它是少数几个不能遵守最小范围规则的地方之一。

    请注意,这似乎取决于编译器版本(鉴于我们正在“未定义”行为区域中行走,您会期望它)

    【讨论】:

      猜你喜欢
      • 2010-12-30
      • 2012-05-22
      • 2017-03-16
      • 2010-10-27
      • 1970-01-01
      • 1970-01-01
      • 2010-12-31
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多