线程安全类的有序静态初始化

时间:2012-01-11 15:47:48

标签: c++ windows thread-safety initialization lazy-initialization

对于最后的简短问题,这篇文章可能看起来过长。但我还需要描述一下我刚想出的设计模式。也许它是常用的,但我从未见过它(或者它可能不起作用)。

首先,这是一个代码(由于我的理解)由于“静态初始化命令惨败”而具有未定义的行为。问题是,西班牙语:: s_englishToSpanish的初始化依赖于English :: s_numberToStr,它们都是静态初始化的并且位于不同的文件中,因此这些初始化的顺序是未定义的:

档案:English.h

#pragma once

#include <vector>
#include <string>

using namespace std;

struct English {
    static vector<string>* s_numberToStr;
    string m_str;

    explicit English(int number)
    {
        m_str = (*s_numberToStr)[number];
    }
};

档案:English.cpp

#include "English.h"

vector<string>* English::s_numberToStr = new vector<string>( /*split*/
[]() -> vector<string>
{
    vector<string> numberToStr;
    numberToStr.push_back("zero");
    numberToStr.push_back("one");
    numberToStr.push_back("two");
    return numberToStr;
}());

档案:Spanish.h

#pragma once

#include <map>
#include <string>

#include "English.h"

using namespace std;

typedef map<string, string> MapType;

struct Spanish {
    static MapType* s_englishToSpanish;
    string m_str;

    explicit Spanish(const English& english)
    {
        m_str = (*s_englishToSpanish)[english.m_str];
    }
};

档案:Spanish.cpp

#include "Spanish.h"

MapType* Spanish::s_englishToSpanish = new MapType( /*split*/
[]() -> MapType
{
    MapType englishToSpanish;
    englishToSpanish[ English(0).m_str ] = "cero";
    englishToSpanish[ English(1).m_str ] = "uno";
    englishToSpanish[ English(2).m_str ] = "dos";
    return englishToSpanish;
}());

档案:StaticFiasco.h

#include <stdio.h>
#include <tchar.h>
#include <conio.h>

#include "Spanish.h"

int _tmain(int argc, _TCHAR* argv[])
{
    _cprintf( Spanish(English(1)).m_str.c_str() ); // may print "uno" or crash

    _getch();
    return 0;
}

要解决静态初始化顺序问题,我们使用构造首次使用的习惯用法,并将这些静态初始化函数设置为local-local:

档案:English.h

#pragma once

#include <vector>
#include <string>

using namespace std;

struct English {
    string m_str;

    explicit English(int number)
    {
        static vector<string>* numberToStr = new vector<string>( /*split*/
        []() -> vector<string>
        {
            vector<string> numberToStr_;
            numberToStr_.push_back("zero");
            numberToStr_.push_back("one");
            numberToStr_.push_back("two");
            return numberToStr_;
        }());

        m_str = (*numberToStr)[number];
    }
};

档案:Spanish.h

#pragma once

#include <map>
#include <string>

#include "English.h"

using namespace std;

struct Spanish {
    string m_str;

    explicit Spanish(const English& english)
    {
        typedef map<string, string> MapT;

        static MapT* englishToSpanish = new MapT( /*split*/
        []() -> MapT
        {
            MapT englishToSpanish_;
            englishToSpanish_[ English(0).m_str ] = "cero";
            englishToSpanish_[ English(1).m_str ] = "uno";
            englishToSpanish_[ English(2).m_str ] = "dos";
            return englishToSpanish_;
        }());

        m_str = (*englishToSpanish)[english.m_str];
    }
};

但现在我们还有另一个问题。由于函数本地静态数据,这些类都不是线程安全的。为了解决这个问题,我们为这两个类添加了一个静态成员变量和一个初始化函数。然后在这个函数内部,通过调用每个具有函数本地静态数据的函数,强制初始化所有函数本地静态数据。因此,实际上我们在程序开始时初始化所有内容,但仍然控制初始化的顺序。所以现在我们的类应该是线程安全的:

档案:English.h

#pragma once

#include <vector>
#include <string>

using namespace std;

struct English {
    static bool s_areStaticsInitialized;
    string m_str;

    explicit English(int number)
    {
        static vector<string>* numberToStr = new vector<string>( /*split*/
        []() -> vector<string>
        {
            vector<string> numberToStr_;
            numberToStr_.push_back("zero");
            numberToStr_.push_back("one");
            numberToStr_.push_back("two");
            return numberToStr_;
        }());

        m_str = (*numberToStr)[number];
    }

    static bool initializeStatics()
    {
        // Call every member function that has local static data in it:
        English english(0); // Could the compiler ignore this line?
        return true;
    }
};
bool English::s_areStaticsInitialized = initializeStatics();

档案:Spanish.h

#pragma once

#include <map>
#include <string>

#include "English.h"

using namespace std;

struct Spanish {
    static bool s_areStaticsInitialized;
    string m_str;

    explicit Spanish(const English& english)
    {
        typedef map<string, string> MapT;

        static MapT* englishToSpanish = new MapT( /*split*/
        []() -> MapT
        {
            MapT englishToSpanish_;
            englishToSpanish_[ English(0).m_str ] = "cero";
            englishToSpanish_[ English(1).m_str ] = "uno";
            englishToSpanish_[ English(2).m_str ] = "dos";
            return englishToSpanish_;
        }());

        m_str = (*englishToSpanish)[english.m_str];
    }

    static bool initializeStatics()
    {
        // Call every member function that has local static data in it:
        Spanish spanish( English(0) ); // Could the compiler ignore this line?
        return true;
    }
};

bool Spanish::s_areStaticsInitialized = initializeStatics();

这就是问题:某些编译器是否可能优化那些具有本地静态数据的函数调用(本例中为构造函数)?所以问题是“具有副作用”的确切含义,据我所知,这意味着编译器不允许对其进行优化。是否有足够的函数本地静态数据使编译器认为函数调用不能被忽略?

3 个答案:

答案 0 :(得分:1)

好的,简而言之:

  1. 我不明白为什么这个类的静态成员需要公开 - 它们是实现细节。

  2. 不要将它们设为私有,而是将它们作为编译单元的成员(实现类的代码将在其中)。

  3. 使用boost::call_once执行静态初始化。

  4. 首次使用时的初始化相对容易强制执行排序,这是一种难以按顺序执行的破坏。但请注意,call_once中使用的函数不得抛出异常。因此,如果它可能失败,你应该留下某种失败的状态并在通话后检查它。

    (我假设在你的实际例子中,你的负载不是你硬编码的东西,但你更有可能加载某种动态表,所以你不能只创建一个内存数组)

答案 1 :(得分:1)

C ++ 11标准的第1.9节“程序执行”[intro.execution]表示

  

1本国际标准中的语义描述定义了参数化的非确定性抽象机器。 ......符合要求的实现需要模拟(仅)抽象机器的可观察行为,如下所述   ......

     

5执行格式良好的程序的符合实现应该产生与具有相同程序和相同输入的抽象机的相应实例的可能执行之一相同的可观察行为。
  ...

     

8对符合要求的实施的最低要求是:
   - 严格按照抽象机的规则评估对易失性对象的访问    - 在程序终止时,写入文件的所有数据应与根据抽象语义产生的程序执行的可能结果之一相同。
   - 交互设备的输入和输出动态应以在程序等待输入之前提示输出实际传送的方式进行。构成交互设备的是实现定义的   这些统称为程序的可观察行为   ...

     

12访问由volatile glvalue(3.10)指定的对象,修改对象,调用库I / O函数或调用执行任何这些操作的函数都是副作用,这是执行环境状态的变化。

此外,在3.7.2“自动存储持续时间”[basic.stc.auto]中,据说

  

3如果具有自动存储持续时间的变量具有初始化或具有副作用的析构函数,则在其块结束之前不应销毁它,也不应将其作为优化消除,即使它看起来是未使用的,除了可以按照12.8中的规定消除类对象或其复制/移动。

12.8-31描述了我认为与此无关的复制省略。

所以问题是局部变量的初始化是否有副作用阻止它被优化掉。由于它可以使用动态对象的地址执行静态变量的初始化,我认为它会产生足够的副作用(例如,修改对象)。你也可以在那里添加一个带有volatile对象的操作,从而引入一个无法消除的可观察行为。

答案 2 :(得分:0)

也许你需要做额外的工作来控制init命令。 等,

class staticObjects
{
    private:
    vector<string>* English::s_numberToStr;
    MapType* s_englishToSpanish;
};

static staticObjects objects = new staticObjects();

然后定义一些接口来检索它。