【问题标题】:Critical sections and the singleton pattern临界区和单例模式
【发布时间】:2012-01-02 03:39:41
【问题描述】:

背景:在函数中使用局部静态变量作为单例模式的实现的一个问题是,如果多个线程同时第一次调用该函数,则静态变量的初始化可以做两次。

我的问题是,如果你将静态变量的初始化包装在一个临界区中,这会阻止双重初始化的发生吗?示例:

CRITICAL_SECTION cs;

Class get_class_instance() {
    EnterCriticalSection(&cs);

    // is the initialisation of c done inside the critical section?
    static Class c = Class(data);

    LeaveCriticalSection(&cs);

    return c;
}

或者是神奇地完成了初始化(不是在声明/初始化的时候),比如在构造函数开始之前初始化变量成员?

我的问题专门针对 C++11 之前的版本,因为根据 Xeo 的回答,C++11 会自行解决这个问题。

【问题讨论】:

  • 通过该编辑,我应该删除我的答案,因为它不再相关。 :P
  • @Xeo 抱歉,我应该更具体一些,但我正在处理的编译器不支持 C++11 的该功能,不过我赞成你的回答,谢谢

标签: c++ multithreading singleton critical-section c++03


【解决方案1】:

C++11 不再需要锁定。如果静态局部变量已被初始化,则并发执行将等待。

§6.7 [stmt.dcl] p4

如果控制在变量初始化的同时进入声明,并发执行将等待初始化完成。


对于 C++03,我们有这个:

§6.7 [stmt.dcl] p4

所有具有静态存储持续时间 (3.7.1) 的本地对象的零初始化 (8.5) 在任何其他初始化发生之前执行。具有用常量表达式初始化的静态存储持续时间的 POD 类型 (3.9) 的本地对象在首次进入其块之前被初始化。允许实现在与 允许实现在命名空间范围内静态初始化具有静态存储持续时间的对象(3.6.2)。 否则这样的对象在控件第一次通过其声明时被初始化;

最后一部分很重要,因为它适用于您的代码。控制第一次进入get_class_instance()时,先经过临界区的初始化,再经过单例的声明(这样会在临界区内初始化),再经过临界区的反初始化。

所以从理论上讲,你的代码应该是安全的。

现在,这可以改进,因为不会在每个函数调用中都进入临界区。 @Chethan 的基本思想是合理的,所以我们将以此为基础。但是,我们也将避免动态分配。然而,为此,我们依赖于 Boost.Optional:

#include <boost/optional.hpp>

Class& get_class_instance() {
    static boost::optional<Class> c;
    static bool inited;

    if (!inited){
        EnterCriticalSection(&cs);

        if(!c)
            c = Class(data);

        LeaveCriticalSection(&cs);
        inited = true;
    }

    return *c;
}

Boost.Optional 避免了默认初始化,双重检查避免在每个函数调用时进入临界区。然而,这个版本在赋值中引入了对 Class 的复制构造函数的调用。解决方案是就地工厂:

#include <boost/utility/in_place_factory.hpp>
#include <boost/optional.hpp>

Class& get_class_instance() {
    static boost::optional<Class> c;
    static bool inited;

    if (!inited){
        EnterCriticalSection(&cs);

        if(!c)
            c = boost::in_place(data);

        LeaveCriticalSection(&cs);
        inited = true;
    }

    return *c;
}

感谢@R。 Martinho Fernandes 和@Ben Voigt 合作完成了这个最终解决方案。如果您对该过程感兴趣,请随时查看transcript


现在,如果您的编译器已经支持某些 C++11 功能,但不支持静态初始化的东西,您还可以使用 std::unique_ptr 结合放置 new 和静态对齐缓冲区:

#include <memory> // std::unique_ptr
#include <type_traits> // alignment stuff

template<class T>
struct destructor{
    void operator(T* p) const{
    if(p) // don't destruct a null pointer
        p->~T();
    }
};

Class& get_class_instance() {
    typedef std::aligned_storage<sizeof(Class),
        std::alignment_of<Class>::value>::type storage_type;
    static storage_type buf;
    static std::unique_ptr<Class, destructor> p;
    static bool inited;
    
    if (!inited){
        EnterCriticalSection(&cs);
    
        if(!p)
            p.reset(new (&buf[0]) Class(data));
    
        LeaveCriticalSection(&cs);
        inited = true;
    }
    
    return *p;
}

【讨论】:

  • 对于静态类成员也是如此吗?
  • 啊,C++11 来救场了。那么 C++11 之前的版本呢?
  • @Seth:Pre-C++11 甚至不知道并发执行。 :P 我对并发的了解还不够,无法对您的想法发表评论。 :(
  • @littleadv:p4 处理块范围变量的初始化。给我一秒钟寻找全球当地人。 (在任何情况下,通过将全局局部放入函数中,都可以轻松地将其转换为块作用域局部。)
  • 如果编译器允许双重初始化,我认为这不会阻止它。要么你没有效果(如果编译器有一个覆盖所有静态本地的单一检查)或可能的死锁(如果编译器对每个本地实现一个检查)。
【解决方案2】:

用关键部分包装初始化肯定会有所帮助!我会使用下面的代码来确保我们的静态变量只初始化一次。

CRITICAL_SECTION cs;

Class& get_class_instance() {
    static Class *c; //by default, global and static variables are assigned default values. Hence c will be NULL when the program starts.

    EnterCriticalSection(&cs);

    if(c == NULL)
        c = new Class(data);

    LeaveCriticalSection(&cs);

    return *c;
}

【讨论】:

  • 哦!是的。我的错。让我们在堆上分配它!
  • 这可以变得更漂亮,而无需锁定每个呼叫。 static char init_forcer = (EnterCriticalSection(&amp;cs), c = new Class(data), LeaveCriticalSection(&amp;cs), 0); 正下方 static Class* c; 行应该可以完成这项工作(未经测试)。
  • @Ben:为什么?该代码仅在控件首次进入函数时执行一次(因为它用于初始化静态本地)。因此,在第一次执行时,c 当然仍然是一个空指针。
  • @Xeo:嗯,是不是有可能两个线程同时进行初始化并且都执行整个练习点?
  • 您从未初始化 CRITICAL_SECTION,这使得这是一个递归问题。
【解决方案3】:

对于 C++03,您需要无锁更新状态。这是因为你不能在没有初始化的情况下使用锁,这将再次递归地遇到完全相同的问题,因为你需要线程安全地初始化你将用于线程安全初始化的锁。哎呀。对于全局变量和一些无锁指令,您只能依赖零初始化。这也快得多。

您可以使用状态变量和无锁 CAS 指令来解决此问题。

enum State {
    ObjectUninitialized,
    ObjectInitializing,
    ObjectInitialized
};

volatile std::size_t state; // Must be initialized to 0- done by compiler
                            // before main(). All globals are 0
                            // Also must be word sized

T* func() {
    static char buffer[sizeof(T)];
    long result = InterlockedCompareExchange(&state, ObjectInitializing, ObjectUninitialized);
    if (result == ObjectInitialized) {
        return reinterpret_cast<T*>(buffer);
    }
    if (result == ObjectInitializing) {
        while (state == ObjectInitializing); // read is atomic for word-size variables
        return reinterpret_cast<T*>(buffer); // and use volatile to force compiler to add     
    }
    if (result == ObjectUninitialized) {
        new (buffer) T();
        InterlockedExchange(&state, ObjectInitialized);
        return reinterpret_cast<T*>(buffer);
    }
}

【讨论】:

    【解决方案4】:

    我不确定您的代码具体是否正确,但通常是的,它会解决问题。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-01-06
      • 2018-10-17
      • 2013-03-26
      • 2013-04-07
      • 2021-09-05
      相关资源
      最近更新 更多