1

我一直在尝试编写一个包装类来包装 Win32 内部函数,InterlockedIncrement例如InterlockedExchange. 尽管我的问题在其他支持类似内在函数的平台上可能类似。

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

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 解决了这个问题,但我无法在我的编译器中访问它......

4

5 回答 5

7

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

于 2013-05-15T13:12:11.320 回答
2

运算符+-是没有意义的。您实际实现的内容看起来更像复合赋值 ( +=, -=),但您需要返回类型的值T而不是对(*this). 当然,这不遵循赋值运算符的约定......std::atomic选择使用命名函数而不是运算符重载除++and之外的所有内容--,可能是出于这个原因。

于 2013-05-15T13:14:10.233 回答
1

您的代码中存在数据竞争

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

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

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

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

基本上你想要做的事情可能是可能的,但真的很难,并且绝对依赖于每个平台上的软件和硬件保证 - 不可能以可移植的方式编写它。

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

于 2013-05-15T13:39:57.293 回答
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;
于 2013-05-15T13:14:59.417 回答
0

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

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

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

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

如果 Thread #2 在 Thread #1 之前立即执行它的并发加法,那么您得到的值为:

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

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

于 2013-05-15T13:31:31.010 回答