首页 > 解决方案 > 如果不可能进行单线程重新排序,则 Interlocked、InterlockedAcquire 和 InterlockedRelease 之间的区别

问题描述

对于我的应用程序而言,无锁实现很可能已经过大了,但我还是想研究一下内存屏障和无锁性,以防将来我真的需要使用这些概念。

据我所知:

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

  2. “InterlockedRelease”函数执行原子操作,同时防止编译器将 InterlockedRelease 之前的代码语句移动到 InterlockedRelease 之后。

  3. 一个普通的“互锁”函数执行原子操作,同时防止编译器在互锁调用的任一方向上移动代码语句。

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

举一个更具体的例子,这是我当前的应用程序——produce() 函数作为最终使用循环缓冲区构建的多生产者、单消费者队列的一部分:

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) 之前还是之后,我应该让编译器决定。

标签: c++c++11winapivisual-studio-2017

解决方案


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

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

考虑这段代码:

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

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

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

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

因此它也可以5从原始代码中看到一个值,即使a永远不是 2!

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

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


推荐阅读