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

发布于 2024-09-03 19:42:56 字数 1167 浏览 9 评论 0原文

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

对于函数本地静态 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

}

这个想法是锁定需要时间,如果引用被多个线程覆盖,那也没关系。

我想知道这

  1. 在实践中是否足够安全?
  2. 根据规则安全吗? (我知道,当前的标准甚至不知道什么是“并发”,但是践踏已经初始化的引用又如何?其他标准(例如 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

  1. safe enough in practice?
  2. 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 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(8

憧憬巴黎街头的黎明 2024-09-10 19:42:56

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

  1. 实践中足够安全吗?

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

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

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

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

This is my second attempt at an answer. I'll only answer the first of your questions:

  1. safe enough in practice?

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.

无敌元气妹 2024-09-10 19:42:56

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

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

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

#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..()          
}

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.

╰つ倒转 2024-09-10 19:42:56

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

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

假设第二部分成立(对象在控制第一次通过其声明时被初始化),您的代码可以被认为是线程安全的。

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

更新

因此,对于调用 the_const_thingysome_type 构造函数,根据规则,您的代码是正确的。

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

So, the relevant part of the spec is 6.7/4:

An implementation is permitted to perform early initialization of other local objects with static storage duration under the same conditions that an implementation is permitted to statically initialize an object with static storage duration in namespace scope (3.6.2). Otherwise such an object is initialized the first time control passes through its declaration; such an object is considered initialized upon the completion of its initialization.

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 for the_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.

无边思念无边月 2024-09-10 19:42:56

我不是标准主义者...

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

仅当您使用另一个“全局”中的该值时,惰性时尚才有意义。

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

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

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

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:

  • 'Singleton' to be register their initialization method in a 'GlobalInitializer' object during library load time
  • 'GlobalInitializer' to be called in 'main' before any thread is launched

though I may not be describing it accurately.

三生一梦 2024-09-10 19:42:56

简而言之,我认为:

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

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

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

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

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

  2. 根据规则安全:嗯,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:

  1. 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.

  2. 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).

浅笑轻吟梦一曲 2024-09-10 19:42:56

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

根据我的经验,您将必须使用指针,而不是引用。

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:

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

In my experience, you will have to use a pointer, not a reference.

浪漫之都 2024-09-10 19:42:56

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

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.

唠甜嗑 2024-09-10 19:42:56

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

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

This seems to be the easiest/cleanest approach I can think of without needing all of the mutex 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();
}
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文