2014-01-17 40 views
3

假设你有一个多态Singleton类型(在我们的例子中是一个自定义的std::error_category类型)。该类型是无状态的,所以没有数据成员,但它确实有一些虚拟功能。在多线程环境中实例化此类型时出现问题。初始化没有魔术静态的空多态Singleton类型

实现这一目标是使用C++ 11的magic statics最简单的方法:

my_type const& instantiate() { 
    static const my_type instance; 
    return instance; 
} 

不幸的是,我们的编译器(VC11)之一不支持此功能。

  • 我应该期望这会在多线程环境中爆炸吗?我很确定,就标准而言,所有投注都是关闭的。但是鉴于这种类型不包含任何数据成员和虚拟函数,我应该从VC11等主流实现中期望什么样的错误?例如,在执行error_category时,Boost.System和VC都没有采​​取任何预防措施。他们只是粗心大意,还是担心这里的比赛是不合理的偏执狂?
  • 以符合标准的方式摆脱数据竞争的最佳方式是什么?由于这种情况下的类型是error_category,我希望尽可能避免执行堆分配。请记住,在这种情况下,Singleton语义是至关重要的,因为错误类别的等同性是由指针比较确定的。这意味着例如线程本地存储不是一个选项。

回答

0

该标准没有提到在多线程上调用函数时如何构造静态的问题。

gcc使用锁来使函数级静态线程安全(可以被标志禁用)。绝大多数(所有?)版本的Visual C++都没有线程安全功能级静态。

建议围绕变量声明使用锁来保证线程安全。

+1

C++ 11 [stmt.dcl/4不是无声:” ......这样的变量是 首次初始化控制穿过它的声明;这样的变量被认为是在 完成初始化如果初始化通过抛出异常退出,则初始化 未完成,因此在下一次控制进入声明时将再次尝试初始化。如果控制在初始化变量时并行输入 ,则并发执行应等待 完成初始化。“ – Casey

1

尝试#2B:实现自己的std::once_flag当量,与atomic<int>Live at Rextester):

my_type const& instantiate() { 
    static std::aligned_storage<sizeof(my_type), __alignof(my_type)>::type storage; 
    static std::atomic_int flag; 

    while (flag < 2) { 
     // all threads spin until the object is properly initialized 
     int expected = 0; 
     if (flag.compare_exchange_weak(expected, 1)) { 
      // only one thread succeeds at the compare_exchange. 
      try { 
       ::new (&storage) my_type; 
      } catch(...) { 
       // Initialization failed. Let another thread try. 
       flag = 0; 
       throw; 
      } 
      // Success! 
      if (!std::is_trivially_destructible<my_type>::value) { 
       std::atexit([] { 
        reinterpret_cast<my_type&>(storage).~my_type(); 
       }); 
      } 
      flag = 2; 
     } 
    } 

    return reinterpret_cast<my_type&>(storage); 
} 

这仅依赖于编译器正确零初始化所有静态存储持续时间的对象,并且还使用了非标准扩展__alignof(<type>)以正确对齐storage,因为微软的编译团队不会被打扰添加没有两个下划线的关键字。


尝试#1:在具有 std::once_flagLive demo at Coliru)结合使用 std::call_once

my_type const& instantiate() { 
    struct empty {}; 
    union storage_t { 
     empty e; 
     my_type instance; 
     constexpr storage_t() : e{} {} 
     ~storage_t() {} 
    }; 

    static std::once_flag flag; 
    static storage_t storage; 

    std::call_once(flag, []{ 
     ::new (&storage.instance) my_type; 
     std::atexit([]{ 
      storage.instance.~my_type(); 
     }); 
    }); 

    return storage.instance; 
} 

std::once_flag默认的构造是constexpr,所以它保证恒定初始化期间来构造。我觉得VC正确执行不断的初始化。编辑:不幸的是,通过VS12的MSVC仍然不支持constexpr,所以这种技术有一些未定义的行为。我会再尝试。

+0

比我想要的多一点代码,但它实现了threadsafety的目标,而不需要在函数定义之外导出任何对象,或者在静态初始化顺序失败中有任何参与。 – Casey

+0

您应该使用atomic_int而不是原子,因为在原子中有一个默认构造函数,它在函数运行时将该标志设置为0。如果发生在上一个线程刚刚设置为1时,则会有两个线程进入临界区。 atomic_int仍然被初始化为0,但是这种情况发生在程序启动时,当竞争条件不可能时。 – Ian

+0

@Ian是的,谢谢,我忘记了MSVC未能正确实施“原子”的简单默认构造。 – Casey

2

这是一个可能更简单的版本凯西的答案,它使用原子自旋锁来防止正常静态声明

my_type const& instantiate() 
{ 
    static std::atomic_int flag; 
    while (flag != 2) 
    { 
    int expected = 0; 
    if (flag.compare_exchange_weak(expected, 1)) 
     break; 
    } 
    try 
    { 
    static my_type instance = whatever; // <--- normal static decl and init 

    flag = 2; 
    return instance; 
    } 
    catch (...) 
    { 
    flag = 0; 
    throw; 
    } 
} 

这段代码也更容易变成三个宏观的重用,这是很容易#defined到没事就支持魔静平台。

my_type const& instantiate() 
{ 
    MY_MAGIC_STATIC_PRE; 

    static my_type instance = whatever; // <--- normal static decl and init 

    MY_MAGIC_STATIC_POST; 

    return instance; 

    MY_MAGIC_STATIC_SCOPE_END; 
} 
+0

@Casey - 感谢您发现。我现在修好了。 – Ian