代码之家  ›  专栏  ›  技术社区  ›  sbi

函数局部静态常量对象的线程安全初始化

  •  19
  • sbi  · 技术社区  · 14 年前

    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,有什么可以说与此相关的吗?)

    我之所以想知道这一点,是因为我想知道我是否可以保持代码的原样,或者我是否需要返回并修复它。


    对于好奇的人:

    我使用的许多这样的函数local static const对象是映射,这些映射在第一次使用时从const数组初始化,并用于查找。例如,我有一些XML解析器,其中标记名字符串被映射到 enum 价值观,所以我以后可以 switch 在标签上 枚举 价值观。


    因为我得到了一些关于该做什么的答案,但是还没有得到我实际问题的答案(见1)。2。上面),我将开始对这个赏金。再一次:
    我对我能做的事不感兴趣 相反 我真的很想知道 .

    8 回复  |  直到 14 年前
        1
  •  14
  •   rjnilsson    14 年前

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

    1. 实践中足够安全?

    不,因为你是在说你自己,你只是在确保对象创建受到保护,而不是对象引用的初始化。

    在没有C++ 98内存模型和没有来自编译器供应商的显式语句的情况下,不能保证将表示实际引用的内存写入到包含引用标志初始化值的内存(如果是它是如何实现的),则以相同的顺序从多个THE中看到。广告。

    正如您所说,用相同的值多次重写引用应该不会产生语义差异(即使存在单词撕裂,这在您的处理器体系结构中通常是不可能的,甚至是不可能的),但有一种情况很重要: 当多个线程在程序执行期间第一次争用调用函数时 . 在这种情况下,一个或多个线程可以在初始化实际引用之前看到正在设置的初始化标志。

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

        2
  •  5
  •   Nikko    14 年前

    这是我的看法(如果在启动线程之前确实无法初始化它):

    我已经看到(并使用)类似的东西来保护静态初始化,使用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_一次,但创建静态变量的线程除外。它将只创建一次,然后再也不会被调用。然后你就没有锁了。

        3
  •  3
  •   R Samuel Klatchko    14 年前

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

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

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

    通过3.6.2,似乎允许的早期初始化正在转换 动态初始化 静态初始化 . 自从 静态初始化 必须发生在任何之前 动态初始化 既然我想不出任何方法来创建线程,直到你 动态初始化 ,这样的早期初始化也将保证构造函数被调用一次。

    更新

    所以,在呼叫 some_type 构造函数 the_const_thingy ,根据规则,您的代码是正确的。

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

        4
  •  0
  •   Matthieu M.    14 年前

    我不是标准化的…

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

    只有当您使用另一个“全局”的值时,懒惰的时尚才有意义。

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

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

    尽管我可能没有准确地描述它。

        5
  •  0
  •   rjnilsson    14 年前

    总之,我认为:

    • 对象初始化是线程安全的,假设在输入“create-const-thingy”时完全构造了“some-mutex”。

    • “use-const-thingy”中的对象引用的初始化不能保证是线程安全的;它可能(如您所说)会被多次初始化(这不是一个问题)。 但它也可能会被撕裂 可能导致行为不明确。

    [我假设C++引用是使用指针值来实现对实际对象的引用,这在理论上可以在部分写入到“读取”时被读取。

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

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

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


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

        6
  •  0
  •   user2356685    6 年前

    我已经编程了足够多的进程间套接字来做噩梦。为了在具有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));
    }
    

    根据我的经验,你必须使用指针,而不是参考。

        7
  •  -1
  •   Puppy    14 年前

    只需在开始创建线程之前调用函数,从而保证引用和对象。或者,不要使用如此糟糕的设计模式。我的意思是,为什么在地球上对一个静止物体有一个静止的参照物?为什么还要有静态物体?这没什么好处。单身是个糟糕的主意。

        8
  •  -1
  •   Chappelle    12 年前

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

    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();
    }