【问题标题】:Thread-safe initialization of function-local static const objects函数局部静态 const 对象的线程安全初始化
【发布时间】:2011-02-26 17:00:05
【问题描述】:

This question 让我质疑我多年来一直遵循的做法。

对于函数局部静态常量对象的线程安全初始化,我保护对象的实际构造,但不保护函数局部的初始化参考 指它。像这样的:

namespace {
  const some_type& create_const_thingy()
  {
     lock my_lock(some_mutex);
     static const some_type the_const_thingy;
     return the_const_thingy;
  }
}

void use_const_thingy()
{
  static const some_type& the_const_thingy = create_const_thingy();

  // use the_const_thingy

}

这个想法是锁定需要时间,如果引用被多个线程覆盖,那就没关系了。

如果是这个我会很感兴趣

  1. 在实践中足够安全吗?
  2. 根据规则安全吗? (我知道,当前的标准甚至不知道“并发”是什么,但是践踏已经初始化的引用呢?其他标准,比如 POSIX,是否有与此相关的内容要说?)

我想知道这个的原因是我想知道我是否可以让代码保持原样,或者我是否需要回去修复这个问题。


对于好奇的人:

我使用的许多这样的函数局部静态 const 对象都是在第一次使用时从 const 数组初始化并用于查找的映射。例如,我有一些 XML 解析器,其中标签名称字符串映射到 enum 值,因此我可以稍后将 switch 覆盖标签的 enum 值。


由于我得到了一些关于该做什么的答案,但还没有得到我实际问题的答案(参见上面的 1. 和 2.),我将开始对此进行赏金。再说一遍:
我对我能做什么而不是感兴趣,我真的很想知道这个。

【问题讨论】:

  • 我看不出您的问题与您引用的问题有何显着不同。以前不是以一种或另一种形式多次问过这个问题吗?例如,stackoverflow.com/questions/1270927/…。
  • @Neil:我不是在问一般的双重检查锁定等,而是特别是关于不保护简单地址的分配。我没有找到任何关于此的信息,但如果它存在,我很乐意参考它。

标签: c++ concurrency multithreading static-initialization


【解决方案1】:

这是我的看法(如果你真的无法在线程启动之前对其进行初始化):

我已经看到(并使用过)这样的东西来保护静态初始化,使用 boost::once

#include <boost/thread/once.hpp>

boost::once_flag flag;

// get thingy
const Thingy & get()
{
    static Thingy thingy;

    return thingy;
}

// create function
void create()
{
     get();
}

void use()
{
    // Ensure only one thread get to create first before all other
    boost::call_once( &create, flag );

    // get a constructed thingy
    const Thingy & thingy = get(); 

    // use it
    thingy.etc..()          
}

据我了解,这种方式所有线程都在 boost::call_once 上等待,除了将创建静态变量的线程。它只会被创建一次,然后再也不会被调用。然后你就没有锁了。

【讨论】:

  • 我有同样的理解,看起来很不错,不过我想知道是否可以将它以某种方式包裹起来,使call_once不暴露并确保它不会被遗忘。
  • 我知道有一些方法可以正确地做到这一点,也许boost::once 是一种廉价的方法。但是,这并不能回答我过去使用的做法是否足以让我不回到旧代码并修复所有问题的问题。
  • 对我来说没关系,它有效,这只是避免锁定开销的解决方案
【解决方案2】:

我不是标准...

但是对于您提到的用途,为什么不在创建任何线程之前简单地初始化它们呢?许多 Singletons 问题是由于人们使用惯用的“单线程”延迟初始化而引起的,而他们可以在加载库时简单地实例化值(就像典型的全局一样)。

只有当你使用来自另一个“全局”的这个值时,懒惰的方式才有意义。

另一方面,我看到的另一种方法是使用某种协调:

  • “Singleton”将在库加载期间在“GlobalInitializer”对象中注册其初始化方法
  • 在启动任何线程之前在“main”中调用“GlobalInitializer”

虽然我描述的可能不准确。

【讨论】:

  • 我也同意,如果你不需要lazy-init,只需在启动任何线程之前初始化它
  • Matthieu:这是几个 MLoC 代码库,主要是开发的,不是线程感知的,需要带到 MT。我按照我描述的方式检查并修复了几十个函数局部静态对象,因为一些代码已经以这种方式编写,我希望代码保持一致。它似乎有效,但我担心它可能会在未来一段时间内中断,这就是我问的原因。在应用程序启动时初始化数十个这样的函数局部静态可能会起作用(如果我可以确定所有这些都被捕获),但不会被想要。 (对于大多数运行,甚至没有一半被初始化。)
【解决方案3】:

因此,规范的相关部分是 6.7/4:

允许实现对具有静态存储持续时间的其他本地对象进行早期初始化,其条件与允许实现在命名空间范围 (3.6.2) 中静态初始化具有静态存储持续时间的对象的条件相同。否则,此类对象在控件第一次通过其声明时被初始化;这样的对象在其初始化完成时被认为已初始化。

假设第二部分成立 (object is initialized the first time control passes through its declaration),您的代码可以被认为是线程安全的。

通读 3.6.2,似乎允许的早期初始化是将 dynamic-initialization 转换为 static-initialization。因为 static-initialization 必须在任何 dynamic-initialization 之前发生,而且我想不出任何方法来创建线程,直到你到达 dynamic-initialization,这样的早期初始化也可以保证构造函数会被调用一次。

更新

因此,关于为the_const_thingy 调用some_type 构造函数,根据规则,您的代码是正确的。

这留下了关于覆盖规范绝对未涵盖的引用的问题。也就是说,如果您愿意假设引用是通过指针实现的(我相信这是最常见的方法),那么您要做的就是用它已经拥有的值覆盖指针。所以我认为这在实践中应该是安全的。

【讨论】:

  • @Christopher:我不这么认为。当前的标准甚至不承认线程的存在,所以我不认为该短语可以这样解释。它特别没有说明任何关于践踏已经初始化的引用。
  • 如果标准不承认线程的存在,你根本无法对安全性做出任何假设......
  • @bdonlan:我在我的问题中写过。你读得对吗?
【解决方案4】:

简而言之,我认为:

  • 对象初始化是线程安全的,假设进入“create_const_thingy”时“some_mutex”已完全构造。

  • “use_const_thingy”内部对象引用的初始化不保证是线程安全的;它可能(如您所说)会被多次初始化(这不是问题),但它也可能会受到单词撕裂的影响,这可能会导致未定义的行为。

[我假设 C++ 引用是作为对使用指针值的实际对象的引用实现的,理论上可以在部分写入时读取]。

所以,试着回答你的问题:

  1. 在实践中足够安全:很有可能,但最终取决于指针大小、处理器架构和编译器生成的代码。这里的关键可能是指针大小的写入/读取是否是原子的。

  2. 按照规则安全:嗯,C++98 中没有这样的规则,抱歉(但你已经知道了)。


更新:发布此答案后,我意识到它只关注实际问题的一小部分深奥,因此决定发布另一个答案而不是编辑内容。我将“按原样”保留内容,因为它与问题有一些相关性(同时也是为了谦虚,提醒我在回答之前要多考虑一下)。

【讨论】:

    【解决方案5】:

    在开始创建线程之前调用该函数,从而保证引用和对象。或者,不要使用如此糟糕的设计模式。我的意思是,到底为什么对静态对象有静态引用?为什么还要有静态对象?这没有任何好处。单例是个糟糕的主意。

    【讨论】:

    • &lt;sigh&gt; “我对我能做什么不感兴趣,我真的很想知道这件事”有什么难理解的?我什至做到了粗体,我也给出了我想知道这个的确切原因......
    • @sbi:哎呀。没想到开场海报居然还在看回复,因为这个话题好像,两周了。
    • 请继续仔细阅读问题。它说海报开始对其进行赏金。 (此外,发布问题的人无论如何都会收到任何新答案的通知。)
    【解决方案6】:

    这是我第二次尝试回答。我只会回答你的第一个问题:

    1. 在实践中足够安全吗?

    没有。正如您所说,您只是确保对象创建受到保护,而不是对对象的引用的初始化。

    在没有 C++98 内存模型且编译器供应商没有明确声明的情况下,无法保证写入表示实际引用的内存和写入保存初始化标志值的内存 (如果这就是它的实现方式),则从多个线程中以相同的顺序查看引用。

    正如您还说的,用相同的值多次覆盖引用应该没有语义差异(即使存在单词撕裂,这在您的处理器架构上通常不太可能,甚至可能是不可能的)但有一种情况是它重要:当多个线程在程序执行期间第一次竞相调用函数时。在这种情况下,这些线程中的一个或多个可能会在实际引用被初始化之前看到初始化标志被设置。

    您的程序中有一个潜在的错误,您需要修复它。至于优化,我相信除了使用双重检查锁定模式之外还有很多优化。

    【讨论】:

    • 您真的是在暗示可能存在首先设置初始化标志然后分配指针的实现吗? 编辑:哦,等等,对。如果底层平台喜欢它,它可以重新排序写入!这确实是对的。我完全没有想到这一点,但这真的让我大吃一惊。如果没有人提出这个论点的错误,我会接受。
    • @sbi:我什至不认为硬件必须对写入进行重新排序;引用和标志可能在内存中分开,足以驻留在不同的缓存行上。
    • 是的,这也可能是个问题。感谢您回答我的问题!
    • 双重检查锁定实际上并不安全。
    【解决方案7】:

    这似乎是我能想到的最简单/最干净的方法,不需要所有互斥锁:

    static My_object My_object_instance()
    {
        static My_object  object;
        return object;
    }
    
    // Ensures that the instance is created before main starts and creates any threads
    // thereby guaranteeing serialization of static instance creation.
    __attribute__((constructor))
    void construct_my_object()
    {
        My_object_instance();
    }
    

    【讨论】:

    • 但那是 C++11,我写这个问题的时候还没有发布。
    • 无论如何,如果我今天正在阅读此主题,我想知道该问题的最佳/当前解决方案。
    • 但是,我特意没有要求“解决方案”。我问我的旧代码是否安全或需要修复。
    【解决方案8】:

    我已经编写了足够多的进程间套接字来做噩梦。为了在具有 DDR RAM 的 CPU 上实现任何线程安全,您必须对数据结构进行缓存行对齐,并将所有全局变量连续打包到尽可能少的缓存行中。

    未对齐的进程间数据和松散封装的全局变量的问题在于它会导致缓存未命中的别名。在使用 DDR RAM 的 CPU 中,(通常)有一堆 64 字节的缓存线。当你加载一个缓存行时,DDR RAM 会自动加载更多的缓存行,但第一个缓存行总是最热的。高速发生的中断所发生的情况是,缓存页面将充当低通滤波器,就像在模拟信号中一样,并且会过滤掉导致完全令人困惑的错误的中断数据,如果你'不知道发生了什么。对于没有紧密打包的全局变量也是如此。如果它占用多个缓存行,它将不同步,除非您拍摄关键进程间变量的快照并将它们传递到堆栈和寄存器以确保数据正确同步。

    .bss 部分(即存储全局变量的位置,将被初始化为全零,但编译器不会为您缓存行对齐数据,您必须自己做,这也可能是使用C++ Construct in Place 的好地方。要了解对齐指针的最快方法背后的数学原理,请阅读this article;我想弄清楚我是否想出了这个技巧。代码如下所示:

    inline char* AlignCacheLine (char* buffer) {
      uintptr_t offset = ((~reinterpret_cast<uintptr_t> (buffer)) + 1) & (63);
      return buffer + offset;
    }
    
    char SomeTypeInit (char* buffer, int param_1, int param_2, int param_3) {
      SomeType type = SomeType<AlignCacheLine (buffer)> (1, 2, 3);
      return 0xff;
    }
    
    const SomeType* create_const_thingy () {
      static char interprocess_socket[sizeof (SomeType) + 63],
                  dead_byte = SomeTypeInit (interprocess_socket, 1, 2, 3);
      return reinterpret_cast<SomeType*> (AlignCacheLine (interprocess_socket));
    }
    

    根据我的经验,您必须使用指针,而不是引用。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2013-09-03
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2017-03-16
      • 1970-01-01
      相关资源
      最近更新 更多