5

Visual Studio 2012 没有实现线程安全静态初始化的 C++11 标准 ( http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2008/n2660.htm )。我有一个本地静态函数,我需要保证它会以线程安全的方式进行初始化。以下在 Visual Studio 2012中不是线程安全的:

struct MyClass
{
    int a;
    MyClass()
    {
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
        a = 5;
    }
};

void foo()
{
    static MyClass instance;
    std::cout << instance.a << '\n';
}

int main()
{
    std::thread a(foo);
    std::thread b(foo);
    a.join();
    b.join();

    system("pause");
}

上述程序在 Visual Studio 2012 上的输出很可能是:

0
5

我需要解决这个问题,我正在尝试找到一种方法来仅使用函数局部静态(没有全局或类级静态)。

我最初的想法是使用互斥锁,但它也遇到了同样的静态初始化线程安全问题。如果我在 foo 中有一个静态 st::mutex ,则第二个线程可能会在它处于无效状态时获得互斥锁的副本。

另一种选择是添加一个 std::atomic_flag 自旋锁。问题是,std::atomic_flag 初始化线程在 Visual Studio 2012 中是否安全?

void foo()
{
    // is this line thread safe?
    static std::atomic_flag lock = ATOMIC_FLAG_INIT;
    // spin lock before static construction
    while (lock.test_and_set(std::memory_order_acquire));
    // construct an instance of MyClass only once
    static MyClass instance;
    // end spin lock
    lock.clear(std::memory_order_release);
    // the following is not thread safe
    std::cout << instance.a << '\n';
}

在上面的代码中,两个线程是否有可能通过自旋锁,或者保证只有一个线程可以通过?不幸的是,我想不出一种简单的方法来测试它,因为我不能在 atomic_flag 初始化程序中放入一些东西来减慢它,就像我在一个类中可以做到的那样。但是,我想确保我的程序不会因为我做了一个无效的假设而崩溃一次。

4

1 回答 1

5

C++11 的第6.7.4节指出,具有静态存储持续时间的变量被初始化为线程安全的:

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

但是 VC++ 2012 或 2013 Preview 都没有实现这一点,所以是的,你需要一些保护来使你的函数线程安全。

ATOMIC_FLAG_INITC++11在第29.7.4节中也提到了这一点:

宏的ATOMIC_FLAG_INIT定义方式应使其可用于将类型对象初始化为atomic_flag清除状态。对于静态持续时间对象,该初始化应该是静态的。

VC++确实正确地实现了这一点。ATOMIC_FLAG_INIT0VC++ 中,并且 VC++ 在应用程序启动时对所有静态变量进行零初始化,而不是在函数调用中。所以,你使用它是安全的,不会有初始化的竞争lock

测试代码:

struct nontrivial
{
    nontrivial() : x(123) {}
    int x;
};

__declspec(dllexport) int next_x()
{
    static nontrivial x;
    return ++x.x;
}

__declspec(dllexport) int next_x_ts()
{
    static std::atomic_flag flag = ATOMIC_FLAG_INIT;

    while(flag.test_and_set());
    static nontrivial x;
    flag.clear();

    return ++x.x;
}

next_x

                mov     eax, cs:dword_1400035E4
                test    al, 1                   ; checking if x has been initialized.
                jnz     short loc_140001021     ; if it has, go down to the end.
                or      eax, 1
                mov     cs:dword_1400035E4, eax ; otherwise, set it as initialized.
                mov     eax, 7Bh                 
                inc     eax                     ; /O2 is on, how'd this inc sneak in!?
                mov     cs:dword_1400035D8, eax ; init x.x to 124 and return.
                retn
loc_140001021:
                mov     eax, cs:dword_1400035D8
                inc     eax
                mov     cs:dword_1400035D8, eax
                retn

next_x_ts

loc_140001032:
                lock bts cs:dword_1400035D4, 0  ; flag.test_and_set().
                jb      short loc_140001032     ; spin until set.
                mov     eax, cs:dword_1400035E0
                test    al, 1                   ; checking if x has been initialized.
                jnz     short loc_14000105A     ; if it has, go down to end.
                or      eax, 1                  ; otherwise, set is as initialized.
                mov     cs:dword_1400035E8, 7Bh ; init x.x with 123.
                mov     cs:dword_1400035E0, eax

loc_14000105A:
                lock btr cs:dword_1400035D4, 0  ; flag.clear().
                mov     eax, cs:dword_1400035E8
                inc     eax
                mov     cs:dword_1400035E8, eax
                retn

您可以在这里看到next_x绝对不是线程安全的,但从next_x_ts不初始化flag变量cs:dword_1400035D4——它在应用程序启动时初始化为零,因此没有竞争并且next_x_ts是线程安全的。

于 2013-08-19T00:29:16.797 回答