提问人:sbi 提问时间:6/2/2010 最后编辑:Communitysbi 更新时间:6/13/2018 访问量:7156
函数局部静态常量对象的线程安全初始化
Thread-safe initialization of function-local static const objects
问:
这个问题让我质疑了我多年来一直遵循的做法。
对于函数局部静态常量对象的线程安全初始化,我保护对象的实际构造,但不保护引用它的函数局部引用的初始化。像这样的东西:
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
}
这个想法是锁定需要时间,如果引用被多个线程覆盖,那就无关紧要了。
如果这是,我会很感兴趣
- 在实践中足够安全?
- 根据规则安全吗?(我知道,当前的标准甚至不知道什么是“并发”,但是践踏已经初始化的引用呢?其他标准,如POSIX,是否与此相关?
我想知道这一点的原因是我想知道我是否可以保持代码不变,或者我是否需要返回并修复此问题。
对于好奇的人:
我使用的许多这样的函数局部静态 const 对象都是映射,它们在第一次使用时从 const 数组初始化并用于查找。例如,我有几个 XML 解析器,其中标记名称字符串映射到值,因此我稍后可以覆盖标记的值。enum
switch
enum
由于我得到了一些关于该怎么做的答案,但还没有得到我的实际问题的答案(见上面的 1 和 2),我将开始赏金。再说一遍:
我对我能做什么不感兴趣,我真的很想知道这件事。
答:
这是我的看法(如果真的不能在线程启动之前初始化它):
我已经看到(并使用)这样的东西来保护静态初始化,使用 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
我不是标准主义者......
但是对于您提到的用途,为什么不在创建任何线程之前简单地初始化它们呢?许多单例问题之所以引起,是因为人们使用惯用的“单线程”延迟初始化,而他们可以在加载库时简单地实例化值(就像典型的全局初始化一样)。
懒惰时尚只有在使用另一个“全局”的这个值时才有意义。
另一方面,我见过的另一种方法是使用某种协调:
- “Singleton”在库加载期间在“GlobalInitializer”对象中注册其初始化方法
- “GlobalInitializer”在启动任何线程之前在“main”中调用
虽然我可能没有准确地描述它。
评论
因此,规范的相关部分是 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
这就留下了关于覆盖引用的问题,而规范绝对没有涵盖该问题。也就是说,如果你愿意假设引用是通过指针实现的(我认为这是最常见的方法),那么你要做的就是用它已经持有的值覆盖指针。所以我的看法是,这在实践中应该是安全的。
评论
简而言之,我认为:
对象初始化是线程安全的,假设在输入“create_const_thingy”时“some_mutex”是完全构造的。
“use_const_thingy”内部对象引用的初始化不保证是线程安全的;它可能(正如你所说)被多次初始化(这不是一个问题),但它也可能受到单词撕裂的影响,这可能导致未定义的行为。
[我假设 C++ 引用是使用指针值实现为对实际对象的引用,理论上可以在部分写入时读取]。
所以,试着回答你的问题:
在实践中足够安全:很有可能,但最终取决于指针大小、处理器架构和编译器生成的代码。这里的关键可能是指针大小的写/读是否是原子的。
根据规则安全:好吧,C++98 中没有这样的规则,对不起(但你已经知道了)。
更新:发布这个答案后,我意识到它只关注真正问题的一小部分深奥的部分,因此决定发布另一个答案而不是编辑内容。我将内容“按原样”保留,因为它与问题有一定的相关性(也为了谦虚自己,提醒我在回答之前多想一想)。
只需在开始创建线程之前调用该函数,从而保证引用和对象。或者,不要使用这种真正糟糕的设计模式。我的意思是,为什么对静态对象有静态引用?为什么还要有静态对象?这没有任何好处。单身是一个糟糕的主意。
评论
<sigh>
“我对我能做什么不感兴趣,我真的很想知道这件事”有什么难理解的?我什至把它加粗了,我还给出了我想知道这个的确切原因......
这是我第二次尝试回答。我只会回答你的第一个问题:
- 在实践中足够安全?
不。正如你自己所说,你只是确保对象创建受到保护,而不是初始化对对象的引用。
在没有 C++ 内存模型且编译器供应商没有显式语句的情况下,无法保证写入表示实际引用的内存和写入包含引用的初始化标志值的内存(如果这是实现方式)的顺序在多个线程中以相同的顺序显示。
正如您所说,使用相同的值多次覆盖引用应该不会产生语义差异(即使在存在单词撕裂的情况下,这在处理器体系结构上通常不太可能,甚至可能是不可能的),但有一种情况很重要:当多个线程在程序执行期间首次竞相调用函数时.在这种情况下,这些线程中的一个或多个线程可能会在初始化实际引用之前看到正在设置的初始化标志。
您的程序中有一个潜在的错误,您需要修复它。至于优化,我敢肯定,除了使用双重检查的锁定模式之外,还有很多优化。
评论
这似乎是我能想到的最简单/最干净的方法,而不需要所有的互斥锁:
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();
}
评论
我已经编写了足够多的进程间套接字来做噩梦。为了在具有 DDR RAM 的 CPU 上实现线程安全,您必须对数据结构进行缓存行对齐,并将所有全局变量连续打包到尽可能少的缓存行中。
未对齐的进程间数据和松散打包的全局变量的问题在于,它会导致缓存未命中导致的别名。在使用 DDR RAM 的 CPU 中,(通常)有一堆 64 字节的缓存行。当您加载缓存行时,DDR RAM 会自动加载更多缓存行,但第一个缓存行始终是最热的。高速发生的中断是缓存页面将充当低通滤波器,就像在模拟信号中一样,如果您不知道发生了什么,它将过滤掉中断数据,从而导致完全令人困惑的错误。同样的事情也适用于没有紧密包装的全局变量;如果它占用了多个缓存行,它将不同步,除非您拍摄关键进程间变量的快照并将它们传递到堆栈和寄存器上,以确保数据正确同步。
.bss 部分(即存储全局变量的位置)将初始化为全零,但编译器不会为您缓存行对齐数据,您必须自己执行此操作,这也可能是使用 C++ 构造的好地方。要了解对齐指针的最快方法背后的数学原理,请阅读本文;我试图弄清楚我是否想出了那个技巧。代码如下所示:
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));
}
根据我的经验,您必须使用指针,而不是引用。
上一个:检测扫描文档中的空页
评论