如何编写安全的原子对象包装器?

时间:2013-05-15 13:09:51

标签: c++

我一直在尝试编写一个包装类来包装Win32内部函数,例如InterlockedIncrementInterlockedExchange。虽然我的问题可能类似于支持类似内在函数的其他平台。

我有一个基本的模板类型:

template <typename T, size_t W = sizeof(T)>
class Interlocked {};

其中部分专用于不同大小的数据类型。例如,这是32位的:

//
// Partial specialization for 32 bit types
//
template<typename T>
class Interlocked <T, sizeof(__int32)>
{
public:

    Interlocked<T, sizeof(__int32)>() {};

    Interlocked<T, sizeof(__int32)>(T val) : m_val(val) {}

    Interlocked<T, sizeof(__int32)>& Interlocked<T, sizeof(__int32)>::operator= (T val)
    {
        InterlockedExchange((LONG volatile *)&m_val, (LONG)val);
        return *this;
    }

    Interlocked<T, sizeof(__int32)> Interlocked<T, sizeof(__int32)>::operator++()
    {
        return static_cast<T>(InterlockedIncrement((LONG volatile *)&m_val));
    }

    Interlocked<T, sizeof(__int32)> Interlocked<T, sizeof(__int32)>::operator--()
    {
        return static_cast<T>(InterlockedDecrement((LONG volatile *)&m_val));
    }

    Interlocked<T, sizeof(__int32)>& Interlocked<T, sizeof(__int32)>::operator+(T val)
    {
        InterlockedExchangeAdd((LONG volatile *)&m_val, (LONG) val);
        return *this;
    }

    Interlocked<T, sizeof(__int32)>& Interlocked<T, sizeof(__int32)>::operator-(T val)
    {
        InterlockedExchangeSubtract((LONG volatile *)&m_val, (LONG) val);
        return *this;
    }

    operator T()
    {
        return m_val;
    }

private:

    T m_val;
};

但是,我得出结论,我不知道如何安全地写这样的对象。具体来说,我意识到在执行互锁操作后返回*this允许另一个线程在返回之前更改变量。这使类型的点无效。写这样的东西有可能吗?据推测std :: atomic解决了这个问题,但我在编译器中无权访问...

5 个答案:

答案 0 :(得分:7)

如果您没有std::atomic,则可以使用boost::atomic(出现在最新的Boost 1.53),这是经过良好测试的跨平台实施。

答案 1 :(得分:2)

运营商+-毫无意义。您实际实施的内容看起来更像是复合作业(+=-=),但您需要返回T类型的值,而不是(*this)的引用。当然,这不遵循赋值运算符的约定... std::atomic选择使用命名函数而不是除++--之外的所有内容的运算符重载。可能是因为这个原因。 / p>

答案 2 :(得分:1)

您的代码中有数据竞争

您可以同时写入变量(使用InterlockedBlah(...))并使用运算符T从中读取。

C ++ 11的内存模型声明不允许这样做。您可能依赖于您的平台的硬件规范,这可能表明4字节(对齐!)读取不会撕裂,但这最多是脆弱的。并且未定义的行为是未定义的。

此外,读取没有任何内存障碍[告诉编译器和硬件]不要重新排序指令。

使读取返回InterlockedAdd(&amp; val,0)操作可能会解决所有这些问题,因为Windows上的Interlocked API可以保证添加正确的内存屏障。但是,请注意其他没有此保证的MS平台上的Interlocked * API。

基本上你想要做的事情可能是可能但非常困难,并且肯定依赖于每个平台上的软件和硬件保证 - 不可能以便携方式编写它。

使用std :: atomic,使用boost :: atomic

答案 3 :(得分:0)

除了从nogard“使用别人已经测试过的和正在实施的实施”的非常好的建议之外,我建议您不要返回*this,但是操作的结果 - 这是如何现有的互锁运算符工作(以及std :: atomic如何工作)。

换句话说,您的操作员代码应如下所示:

T Interlocked<T, sizeof(__int32)>::operator+(T val)
{
    return InterlockedExchangeAdd((LONG volatile *)&m_val, (LONG) val);
}

有一个问题,正如Ben Voigt所说,这个函数修改了输入值,这意味着:

a = b + c;

实际上会这样做:

b += c; 
a = b;

答案 4 :(得分:0)

考虑两个线程在您的原子序数类上执行并发添加,其中线程#n将数量t_n添加到您的数字x

您担心在执行添加和在一个线程中返回结果之间,第二个线程可能会执行添加,从而弄乱第一个线程的返回值。

该类用户的观察行为是返回值为(x + t_1 + t_2)而不是预期的(x + t_1)

现在让我们假设您有一个不允许该行为的实现,即结果保证为(x_1 + t_1),其中x_1是紧接在线程#1执行其之前的数字的值此外。

如果线程#2在线程#1之前立即执行并发添加,则获得的值为:

(x_1 + t_1) = ((x + t_2) + t_1)

完全相同的种族。除非你在应用添加之前引入一些额外的同步或检查数字的预期值,否则你将永远得到这场比赛。