我一直在尝试编写一个包装类来包装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解决了这个问题,但我在编译器中无权访问...
答案 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)
完全相同的种族。除非你在应用添加之前引入一些额外的同步或检查数字的预期值,否则你将永远得到这场比赛。