如果单线程无法重新排序,则Interlocked、InterlockedAcquire和InterlockedRelea

Difference between Interlocked, InterlockedAcquire, and InterlockedRelease if single thread reordering is impossible

本文关键字:Interlocked InterlockedAcquire InterlockedRelea 排序 单线程 新排序 如果      更新时间:2024-09-27

很可能,对于我的应用程序来说,无锁实现已经有些过头了,但无论如何,我都想研究内存障碍和无锁性,以防将来真正需要使用这些概念。

据我所知:

  1. ;InterlockedAcquire";函数执行原子操作,同时防止编译器将代码语句从InterlockedAcquire之后移动到Interlockedcquire之前。

  2. 一个";联锁释放酶";函数执行原子操作,同时防止编译器将InterlockedRelease之前的代码语句移动到InterlockedRelase之后。

  3. 香草";互锁的";函数执行原子操作,同时防止编译器在Interlocked调用中向任意方向移动代码语句。

我的问题是,如果一个函数的结构使得编译器无论如何都不能重新排序任何代码,因为这样做会影响单线程行为,那么Interlocked函数的任何变体之间是否存在差异,或者它们实际上都是一样的?它们之间唯一的区别是如何与代码重新排序交互吗?

对于一个更具体的例子,这里是我当前的应用程序-production()函数,它最终将成为一个使用循环缓冲区构建的多生产者、单消费者队列:

template <typename T>
class Queue {
private:
long headIndex;
long tailIndex;
T* array[MAXQUEUESIZE];
public:
Queue() {
headIndex = 0;
tailIndex = 0;
memset(array, 0, MAXQUEUESIZE*sizeof(void*);
}
~Queue() {
}
bool produce(T value) {
//1) prevents concurrent calls to produce() from causing corruption:
long indexRetVal;
long reservedIndex;
do {
reservedIndex = tailIndex;
indexRetVal = InterlockedCompareExchange64(&tailIndex, (reservedIndex + 1) % MAXQUEUESIZE, reservedIndex);
} while (indexRetVal != reservedIndex);
//2) allocates the node.
T* newValPtr = (T*) malloc(sizeof(T));
if (newValPtr == null) {
OutputDebugString("Queue: malloc returned null");
return false;
}
*newValPtr = value;
//3) prevents a concurrent call to consume from causing corruption by atomically replacing the old pointer:
T* valPtrRetVal = InterlockedCompareExchangePointer(array + reservedIndex, newValPtr, null);
//if the previous value wasn't null, then our circular buffer overflowed:
if (valPtrRetVal != null) {
OutputDebugString("Queue: circular buffer overflowed");
free(newValPtr); //as pointed out by RbMm
return false;
}
//otherwise, everything worked fine
return true;
}
};

据我所知,无论我做什么,3)都会发生在1)和2)之后,但我应该将1)更改为InterlockedRelease,因为我不在乎它是发生在2)之前还是之后,我应该让编译器来决定。

我的问题是,如果一个函数的结构使得编译器无论如何都不能对任何代码进行重新排序,因为这样做会影响单线程行为,那么Interlocked函数的任何变体之间是否存在差异,或者它们实际上都是相同的?它们之间唯一的区别是如何与代码重新排序交互吗?

您可能会将C++语句指令混淆。你的问题不是特定于CPU的,所以你必须假装不知道CPU指令是什么样子的。

考虑这个代码:

if (a == 2)
{
b = 5;
}

现在,这里有一个不影响单个线程的代码重新排序的例子:

int c = b;
b = 5;
if (a != 2)
b = c;

这执行相同的操作,但顺序不同。它对单线程代码没有影响。但是,当然,如果另一个线程正在访问b,它可以从该代码中看到5的值,即使a从来都不是2

因此,即使a从不为2,它也可以从原始代码中看到5的值

为什么,因为从单个线程的角度来看,代码的两个位执行相同的操作。除非使用具有保证线程语义的操作,否则编译器、CPU、缓存和其他平台组件都需要保留这些。

因此,最有可能的是,您认为重新排序任何代码都会影响单线程行为的想法可能是不正确的。有很多方法可以重新排序和优化代码,而不会影响单线程行为。

msdn上有一个文档解释了区别:获取和发布语义。

样品:

a++;
b++;
c++;
  • 如果我们使用获取语义来增加a,那么其他处理器总是在bc的增量之前看到a的增量
  • 如果我们使用释放语义来增加c,那么其他处理器总是在c的增量之前看到ab的增量
  • 执行的InterlockedXxx例程默认具有获取和释放语义

更具体,针对4个值:

a++;
b++;
c++;
d++;
  • 如果我们使用获取语义来增加b,那么其他处理器总是在cd的增量之前看到b的增量;顺序可以是a->b->c,db->a,c,d
  • 如果我们使用释放语义来增加c,那么其他处理器总是在c的增量之前看到ab的增量;顺序可以是a,b->c->da,b,d->c

引用@antiduh:的回答

Acquire表示"只为我之后的事情担心";。新闻稿说";只有担心我面前的事情";。把这两者结合起来就是完整的记忆障碍

这三个版本都阻止编译器在函数调用中移动代码,但编译器并不是唯一进行重新排序的地方。

现代CPU具有";无序执行";甚至";投机执行";。获取和释放语义使代码编译为具有控制CPU内重新排序的标志或前缀的指令。