20

这个问题让我质疑我多年来一直遵循的做法。

对于函数局部静态 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,是否有与此相关的内容要说?)

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


对于好奇的头脑:

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


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

4

8 回答 8

14

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

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

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

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

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

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

于 2010-06-22T06:23:51.983 回答
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 上等待,除了将创建静态变量的线程。它只会被创建一次,然后再也不会被调用。然后你就没有锁了。

于 2010-06-02T09:14:00.303 回答
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。由于静态初始化必须在任何动态初始化之前发生,而且在你到达动态初始化之前我想不出任何方法来创建线程,所以这样的早期初始化也可以保证构造函数会被调用一次。

更新

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

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

于 2010-06-21T06:51:13.657 回答
0

我不是标准主义者...

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

懒惰的方式只有在你使用另一个“全局”的值时才有意义。

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

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

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

于 2010-06-02T09:29:04.087 回答
0

简而言之,我认为:

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

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

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

因此,尝试回答您的问题:

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

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


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

于 2010-06-21T06:53:14.750 回答
0

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

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

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

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

根据我的经验,您将不得不使用指针,而不是参考。

于 2018-06-13T03:23:02.677 回答
-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();
}
于 2012-06-12T20:10:50.687 回答
-1

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

于 2010-06-21T07:44:02.047 回答