函数本地静态const对象的线程安全初始化

时间:2010-06-02 08:04:00

标签: c++ concurrency multithreading static-initialization

This question让我质疑我多年来一直关注的做法。

对于函数本地静态const对象的线程安全初始化,我保护对象的实际构造,但不保护函数的初始化引用引用它。像这样:

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,有什么可说的与此相关吗?)
  3. 我想知道这个的原因是我想知道我是否可以保留代码,或者是否需要返回并解决此问题。


    对于探究者来说:

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


    由于我得到了一些关于该做什么的答案,但是没有得到我实际问题的答案(见上面的1.和2.),我会对此开始赏金。还是那句话:
    我对我能做什么而不是感兴趣,我真的很想知道 this

8 个答案:

答案 0 :(得分:14)

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

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

没有。当你说明自己时,你只是确保对象创建受到保护,而不是初始化对象的引用。

如果没有C ++ 98内存模型且编译器供应商没有明确的语句,则无法保证写入代表实际引用的内存以及写入包含初始化标志值的内存(如果它是如何实现的),参考从多个线程以相同的顺序看到。

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

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

答案 1 :(得分:5)

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

我已经看过(和使用过)这样的东西来保护静态初始化,使用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上等待,除了会创建静态变量的线程。它只会创建一次,然后再也不会被调用。然后你再也没有锁了。

答案 2 :(得分:3)

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

  

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

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

阅读3.6.2,似乎允许的早期初始化是将动态初始化转换为静态初始化。由于静态初始化必须在任何动态初始化之前发生,因为在进行动态初始化<之前我无法想到创建线程的任何方法/ em>,这样的早期初始化也可以保证构造函数可以被调用一次。

<强>更新

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

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

答案 3 :(得分:0)

我不是标准人......

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

如果你使用另一个'全局'中的这个值,懒惰的方式才有意义。

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

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

虽然我可能没有准确地描述它。

答案 4 :(得分:0)

简而言之,我认为:

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

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

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

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

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

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


  3. 更新:在发布此答案后,我意识到它只关注真实问题的一个小的,深奥的部分,因此决定发布另一个答案而不是编辑内容。我将这些内容“按原样”保留,因为它与问题有一定关系(并且还要谦虚自己,提醒我在回答之前再仔细思考一下)。

答案 5 :(得分:0)

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

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

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

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

答案 6 :(得分:-1)

在开始创建线程之前调用该函数,从而保证引用和对象。或者,不要使用这种真正可怕的设计模式。我的意思是,为什么在地球上有静态引用静态对象?为什么甚至有静态物体?这没有任何好处。单身人士是一个糟糕的主意。

答案 7 :(得分:-1)

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

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