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

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

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

首先,这里的代码(据我理解)由于“静态初始化顺序失败”而具有未定义的行为。问题在于,Spanish::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;
}());

文件:西类牙语.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];
    }
};

文件:西类牙语.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;
}

为了解决静态初始化顺序问题,我们使用“首次使用时构造”习惯用法,并使这些静态初始化成为函数本地函数,如下所示:

文件: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];
    }
};

文件:西类牙语.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();

文件:西类牙语.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();

问题是:某些编译器是否可能优化那些对具有本地静态数据的函数(在本例中为构造函数)的调用?所以问题是“有副作用”到底是什么,据我理解,这意味着编译器不允许将其优化掉。函数局部静态数据是否足以让编译器认为函数调用不能被忽略?

最佳答案

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

1 The semantic descriptions in this International Standard define a parameterized nondeterministic abstract machine. ... conforming implementations are required to emulate (only) the observable behavior of the abstract machine as explained below.
...

5 A conforming implementation executing a well-formed program shall produce the same observable behavior as one of the possible executions of the corresponding instance of the abstract machine with the same program and the same input.
...

8 The least requirements on a conforming implementation are:
— Access to volatile objects are evaluated strictly according to the rules of the abstract machine.
— At program termination, all data written into files shall be identical to one of the possible results that execution of the program according to the abstract semantics would have produced.
— The input and output dynamics of interactive devices shall take place in such a fashion that prompting output is actually delivered before a program waits for input. What constitutes an interactive device is implementation-defined.
These collectively are referred to as the observable behavior of the program.
...

12 Accessing an object designated by a volatile glvalue (3.10), modifying an object, calling a library I/O function, or calling a function that does any of those operations are all side effects, which are changes in the state of the execution environment.

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

3 If a variable with automatic storage duration has initialization or a destructor with side effects, it shall not be destroyed before the end of its block, nor shall it be eliminated as an optimization even if it appears to be unused, except that a class object or its copy/move may be eliminated as specified in 12.8.

12.8-31 描述了复制省略,我认为这与这里无关。

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

关于c++ - 线程安全类的有序静态初始化,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/8822229/

相关文章:

c++ - 我如何使用模板化的 typedefs,它们是_in_一个类,来自类外部(例如,由另一个类),与 boost::graph 相关

c++ - 将类对象传递给两个对象

c++ - Boost::shared_ptr 使用这个实例化

c++ - 悬停子窗口时更改光标

c - 如何让多个线程互不干扰地读取多个文件?

c++ - cppunit 测试框架是否有不同的版本?如果是,那么它的最新版本是什么?

windows - 在没有 SSH 的情况下设置 Git

windows - 控制面板项目图标 (Windows XP)

java - 累加器是线程安全的吗?

java - 在同步方法中读取值时的安全发布