函数局部静态 const 对象的线程安全初始化
对于函数本地静态 const 对象的线程安全初始化,我保护对象的实际构造,但不保护函数本地引用的初始化 em> 指的是它。类似这样的:
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
值,因此我可以稍后切换
标记的 enum
价值观。
由于我得到了一些关于该怎么做的答案,但还没有得到我实际问题的答案(参见上面的 1. 和 2.),所以我将对此开始悬赏。再次:
我对我能做什么不感兴趣相反,我真的很想知道这个。
This question made me question a practice I had been following for years.
For thread-safe initialization of function-local static const objects I protect the actual construction of the object, but not the initialization of the function-local reference referring to it. Something like this:
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
}
The idea is that locking takes time, and if the reference is overwritten by several threads, it won't matter.
I'd be interested if this is
- safe enough in practice?
- safe according to The Rules? (I know, the current standard doesn't even know what "concurrency" is, but what about trampling over an already initialized reference? And do other standards, like POSIX, have something to say that's relevant to this?)
The reason I want to know this is that I want to know whether I can leave the code as it is or whether I need to go back and fix this.
For the inquiring minds:
Many such function-local static const objects I used are maps which are initialized from const arrays upon first use and used for lookup. For example, I have a few XML parsers where tag name strings are mapped to enum
values, so I could later switch
over the tags' enum
values.
Since I got some answers as to what to do instead, but haven't got an answer to my actual questions (see 1. and 2. above), I'll start a bounty on this. Again:
I am not interested in what I could do instead, I do really want to know about this.
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(8)
这是我第二次尝试回答。我只回答你的第一个问题:
不。正如您所说,您只是确保对象创建受到保护,而不是对象引用的初始化。
在没有 C++98 内存模型并且编译器供应商没有明确声明的情况下,无法保证写入代表实际引用的内存以及写入保存初始化标志值的内存(如果是)它是如何实现的)以供参考,从多个线程中以相同的顺序看到。
正如您还所说,使用相同的值多次覆盖引用应该不会产生语义差异(即使存在单词撕裂,这在您的处理器架构上通常不太可能甚至可能是不可能的),但有一种情况很重要:< em>当多个线程在程序执行过程中第一次竞相调用该函数时。在这种情况下,这些线程中的一个或多个可能会在初始化实际引用之前看到已设置的初始化标志。
您的程序中有一个潜在的错误,您需要修复它。至于优化,我确信除了使用双重检查锁定模式之外还有很多优化。
This is my second attempt at an answer. I'll only answer the first of your questions:
No. As you're stating yourself you're only ensuring that the object creation is protected, not the initialization of the reference to the object.
In absence of a C++98 memory model and no explicit statements from the compiler vendor, there are no guarantees that writing to the memory representing the actual reference and the writing to the memory that holds the value of the initialization flag (if that is how it is implemented) for the reference are seen in the same order from multiple threads.
As you also say, overwriting the reference several times with the same value should make no semantic difference (even in the presence of word tearing, which is generally unlikely and perhaps even impossible on your processor architecture) but there's one case where it matters: When more than one thread races to call the function for the first time during program execution. In this case it is possible for one or more of these threads to see the initialization flag being set before the actual reference is initialized.
You have a latent bug in your program and you need to fix it. As for optimizations I'm sure there are many besides using the double-checked locking pattern.
这是我的看法(如果你真的无法在线程启动之前初始化它):
我见过(并使用过)类似的东西来保护静态初始化,使用 boost::once
在我的理解中,这样所有线程都会等待boost::call_once 除外,它将创建静态变量。它只会被创建一次,然后就不会再被调用。然后你就不再有锁了。
Here is my take (if really you can't initialize it before threads are launched):
I've seen (and used) something like this to protect static initialization, using boost::once
In my understanding, this way all threads wait on boost::call_once except one that will create the static variable. It will be created only once and then will never be called again. And then you have no lock any more.
因此,规范的相关部分是 6.7/4:
假设第二部分成立(
对象在控制第一次通过其声明时被初始化
),您的代码可以被认为是线程安全的。通读 3.6.2,似乎允许的早期初始化正在将动态初始化转换为静态初始化。由于静态初始化必须在任何动态初始化之前发生,并且因为我想不出任何方法来创建线程,直到动态初始化< /em>,这样的早期初始化也将保证构造函数被调用一次。
更新
因此,对于调用
the_const_thingy
的some_type
构造函数,根据规则,您的代码是正确的。这就留下了关于覆盖引用的问题,而规范绝对没有涵盖这一点。也就是说,如果您愿意假设引用是通过指针实现的(我认为这是最常见的方法),那么您要做的就是用指针已经保存的值覆盖它。所以我的看法是这在实践中应该是安全的。
So, the relevant part of the spec is 6.7/4:
Assuming the second part holds (
object is initialized the first time control passes through its declaration
), your code can be considered thread safe.Reading through 3.6.2, it appears the early initialization permitted is converting dynamic-initialization to static-initialization. Since static-initialization must happen before any dynamic-initialization and since I can't think of any way to create a thread until you get to dynamic-initialization, such an early initialization would also guarantee the constructor would get called a single time.
Update
So, in respect to calling the
some_type
constructor forthe_const_thingy
, your code is correct according to the rules.This leaves the issue about overwriting the reference which is definitely not covered by the spec. That said, if you are willing to assume that references are implemented via pointers (which I believe is the most common way to do that), then all you are going to do is overwrite a pointer with the value that it already holds. So my take is that this should be safe in practice.
我不是标准主义者...
但是对于您提到的用途,为什么不在创建任何线程之前简单地初始化它们?许多单例问题是由于人们使用惯用的“单线程”延迟初始化而引起的,而他们可以在加载库时简单地实例化该值(就像典型的全局变量一样)。
仅当您使用另一个“全局”中的该值时,惰性时尚才有意义。
另一方面,我见过的另一种方法是使用某种协调:
尽管我可能没有准确地描述它。
I am not standardista...
But for the use you mention, why not simply initialize them before any thread is created ? Many Singletons issues are caused because people use the idiomatic "single thread" lazy initialization while they could simply instantiate the value when the library is loaded (like a typical global).
The lazy fashion only makes sense if you use this value from another 'global'.
On the other hand, another method I've seen was to use some kind of coordination:
though I may not be describing it accurately.
简而言之,我认为:
对象初始化是线程安全的,假设“some_mutex”在进入“create_const_thingy”时完全构造完毕。
“use_const_thingy”内部对象引用的初始化不保证是线程安全的;它可能(如您所说)会被多次初始化(这不是什么问题),但它也可能会受到单词撕裂的影响,这可能会导致未定义的行为。
[我假设 C++ 引用被实现为使用指针值对实际对象的引用,理论上可以在部分写入时读取]。
因此,尝试回答您的问题:
实践中足够安全:很有可能,但最终取决于指针大小、处理器架构和编译器生成的代码。这里的关键可能是指针大小的写/读是否是原子的。
根据规则安全:嗯,C++98 中没有这样的规则,抱歉(但你已经知道了)。
根据
更新:发布此答案后,我意识到它只关注真正问题的一小部分,深奥的部分,因此决定发布另一个答案而不是编辑内容。我将内容保留为“原样”,因为它与问题有一定的相关性(也是为了谦虚自己,提醒我在回答之前要多思考一下)。
In brief, I think that:
The object initialization is thread-safe, assuming that "some_mutex" is fully constructed when entering "create_const_thingy".
The initialization of the object reference inside "use_const_thingy" is not guaranteed to be thread-safe; it might (as you say) be subject of getting initialized multiple times (which is less of a problem), but it might also be subject to word tearing which could result in undefined behaviour.
[I assume that the C++ reference is implemented as a reference to the actual object using a pointer value, which could in theory be read when partially written to].
So, to try and answer your question:
Safe enough in practice: Very likely, but ultimately depends on pointer size, processor architecture and code generated by the compiler. The crux here is likely to be whether a pointer-sized write/read is atomic or not.
Safe according to the rule: Well, there are no such rules in C++98, sorry (but you knew that already).
Update: After posting this answer I realized that it only focuses on a small, esoteric part of the real problem, and because of this decided to post another answer instead of editing the contents. I'm leaving the contents "as-is" as it has some relevance to the question (and also to humble myself, reminding me to think through things a bit more before answering).
我编写的进程间套接字已经足够让我做噩梦了。为了在具有 DDR RAM 的 CPU 上实现线程安全,您必须对数据结构进行缓存行对齐,并将所有全局变量连续打包到尽可能少的缓存行中。
未对齐的进程间数据和松散打包的全局变量的问题是,它会导致缓存未命中造成别名。在使用 DDR RAM 的 CPU 中,(通常)有一堆 64 字节缓存线。当您加载一个缓存行时,DDR RAM 会自动加载更多的缓存行,但第一个缓存行始终是最热的。高速发生的中断会发生什么情况,缓存页面将充当低通滤波器,就像模拟信号一样,并且会过滤掉导致完全令人困惑的错误的中断数据,如果您不知道发生了什么事。对于未紧密包装的全局变量也是如此。如果它占用多个缓存行,它将不同步,除非您拍摄关键进程间变量的快照并将它们传递到堆栈和寄存器以确保数据正确同步。
.bss 部分(即存储全局变量的位置)将被初始化为全零,但编译器不会为您缓存行对齐数据,您必须自己执行此操作,这也可能是一个好地方使用C++ 就地构造来学习背后的数学。对齐指针的最快方法请阅读这篇文章; 我试图弄清楚我是否想出了这个技巧,代码如下:
根据我的经验,您将必须使用指针,而不是引用。
I've programmed enough interprocess sockets to have nightmares. In order to make anything thread-safe on a CPU with DDR RAM, you have to cache-line-align the data structure and pack up all of your global variables contiguously into as few cache lines as possible.
The problem with unaligned interprocess data and loosely packed globals are that it causes aliasing from cache misses. In CPUs that use DDR RAM, there are a (usually) a bunch of 64-byte cache lines. When you load a cache line, the DDR RAM will automatically load up a bunch more cache lines, but the first cache line is always the hottest. What happens with interrupts that occur at high speeds is that the cache page will act as a low-pass filter, just like in analog signals, and will filter out the interrupt data leading to COMPLETELY baffling bugs if you're not aware of whats going on. That same thing goes for global variables that are not packed up tightly; if it takes up multiple cache lines it will get out of sync unless you to take a snapshot of the critical interprocess variables and pass them through on the stack and the registers to ensure the data is synced up right.
The .bss section (i.e. where the global variables are stored, will get initialized to all zeros, but the compiler will not cache-line-align the data for you, you will have to do that yourself, which may also a good place to use the C++ Construct in Place. To learn the math behind fastest way to align pointers read this article; I'm trying to figure out if I came up with that trick. Here is what the code will look like:
In my experience, you will have to use a pointer, not a reference.
只需在开始创建线程之前调用该函数,即可保证引用和对象。或者,不要使用如此糟糕的设计模式。我的意思是,为什么要对静态对象有静态引用?为什么还要有静态对象?这没有任何好处。单例是一个糟糕的主意。
Just call the function before you start creating threads, thus guaranteeing the reference and the object. Alternatively, don't use such a truly terrible design pattern. I mean, why on earth have a static reference to a static object? Why even have static objects? There's no benefit to this. Singletons are a terrible idea.
这似乎是我能想到的最简单/最干净的方法,而不需要所有的互斥量:
This seems to be the easiest/cleanest approach I can think of without needing all of the mutex shananigans: