mutex_unlock函数是否作为内存屏障?

10
这里描述的情况发生在 iPad 4 (ARMv7s)上,使用 posix 库进行 mutex 锁/解锁。我在其他 ARMv7 设备上也看到了类似的情况(见下文),因此我认为任何解决方案都需要更一般地查看 ARMv7 的 mutex 和内存屏障的行为。
以下是该场景的伪代码: 线程 1 - 生产数据:
void ProduceFunction() {
  MutexLock();
  int TempProducerIndex = mSharedProducerIndex; // Take a copy of the int member variable for Producers Index
  mSharedArray[TempProducerIndex++] = NewData; // Copy new Data into array at Temp Index 
  mSharedProducerIndex = TempProducerIndex; // Signal consumer data is ready by assigning new Producer Index to shared variable
  MutexUnlock();
}

线程2 - 消费数据:

void ConsumingFunction () {
  while (mConsumerIndex != mSharedProducerIndex) {
    doWorkOnData (mSharedArray[mConsumerIndex++]);
  }
}

之前(当iPad 2上出现问题时),我认为mSharedProducerIndex = TempProducerIndex没有原子性执行,因此改用AtomicCompareAndSwap来分配mSharedProducerIndex。这一直有效,但事实证明我错了,错误又回来了。我猜这个“修复”只是改变了一些时间。
我现在得出的结论是,在互斥锁内部写入的执行顺序是不正确的,即如果编译器或硬件决定重新排序:
mSharedArray[TempProducerIndex++] = NewData; // Copy new Data into array at Temp Index 
mSharedProducerIndex = TempProducerIndex;  // Signal consumer data is ready by assigning new Producer Index to shared variable

...转换为:

mSharedProducerIndex = TempProducerIndex; // Signal consumer data is ready by assigning new Producer Index to shared variable
mSharedArray[TempProducerIndex++] = NewData; // Copy new Data into array at Temp Index 

我认为如果消费者在生产者之间插入,当消费者尝试读取数据时,数据还没有被写入。

在阅读了一些关于内存屏障的资料后,我决定尝试将信号移动到mutex_unlock之外的消费者处,因为我相信解锁会产生一个内存屏障/栅栏,确保mSharedArray已经被写入:

mSharedArray[TempProducerIndex++] = NewData;  // Copy new Data into array at Temp Index 
MutexUnlock();
mSharedProducerIndex = TempProducerIndex; // Signal consumer data is ready by assigning new Producer Index to shared variable

然而,这仍然失败了,这让我怀疑mutex_unlock是否一定会作为写入屏障?

我也读过HP的一篇文章,其中提到编译器可以将代码移动到(但不能移出)crit_sec,因此即使进行了上述更改,mSharedProducerIndex的写入可能仍在屏障之前。这个理论有用吗?

通过添加显式屏障,问题得以解决:

mSharedArray[TempProducerIndex++] = NewData; // Copy new Data into array at Temp Index 
OSMemoryBarrier();
mSharedProducerIndex = TempProducerIndex; // Signal consumer data is ready by assigning new Producer Index to shared variable

因此,我认为我理解了问题,并且需要一个屏障,但对于解锁行为为何不表现为屏障的任何见解都将非常有用。

编辑:

关于消费者线程中缺少互斥量的情况:我依靠写入int mSharedProducerIndex是单个指令,因此希望消费者会读取新值或旧值。两种状态都是有效的,只要mSharedArray按顺序写入(即在写入mSharedProducerIndex之前),这就没问题了,但从迄今为止所说的内容来看,我不能保证这一点。

按照同样的逻辑,目前的屏障解决方案似乎也有缺陷,因为mSharedProducerIndex的写入可能会移动到屏障内,因此可能会被错误地重新排序。

是否建议向消费者添加一个互斥量,仅作为读屏障,或者是否有一个pragma或指令可以禁用生产者上的乱序执行,例如 PPC 上的 EIEIO


2
正如Steve所建议的那样,在读取mSharedProducerIndex之后,您可以立即执行OSMemoryBarrier()。以前的测试表明,OSMemoryBarrier()比OSSpinLockLock+Unlock()要快一些,而OSSpinLockLock+Unlock()比pthread mutexes要快得多。 - tc.
2个回答

7
您的产品已同步,但在消费时您没有进行任何同步(您需要使用屏障将内存同步),因此即使您为生产者设置了完美的内存屏障,这些内存屏障也无法帮助消费者。
在您的代码中,您可能会受到编译器排序、硬件排序甚至其他运行Thread #2的核心上的mSharedProducerIndex的过期值的影响。
您应该阅读Cortex™-A 系列程序员指南第 11 章:内存排序,特别是11.2.1 内存屏障使用示例
我认为您的问题是在消费者线程中获得了部分更新。问题在于生产者关键部分内不是原子的,并且它可以被重新排序。
通过非原子,我的意思是如果您的mSharedArray[TempProducerIndex++] = NewData;不是字存储(NewData 的类型为 int),它可能会分成几个步骤完成,其他核心可以看到这些步骤的部分更新。
通过重新排序,我的意思是互斥体在进入和退出时提供屏障,但在关键部分期间不强制任何排序。由于您在消费者端没有任何特殊的构造,因此您可以看到mSharedProducerIndex已更新,但仍然可以看到对mSharedArray[mConsumerIndex]的部分更新。互斥锁仅在执行离开关键部分后保证内存可见性。
我认为这也解释了为什么在关键部分中添加OSMemoryBarrier();时它能够正常工作,因为这样 CPU 就被强制将数据写入mSharedArray,然后更新mConsumerIndex,当其他核心/线程看到mConsumerIndex时,我们知道由于屏障,mSharedArray已完全复制。
我认为您使用OSMemoryBarrier();的实现是正确的,假设您有多个生产者和一个消费者。我不同意任何建议在消费者中放置内存屏障的评论,因为我认为这不会修复在生产者关键部分中发生的部分更新或重新排序。
作为对标题中问题的回答,一般而言,mutex 在进入之前有读屏障,在离开之后有写屏障。

谢谢你迄今为止的输入,我对问题进行了编辑,希望能够更清晰地阐明。 - sam-w
@sjwarner 如果 mSharedArray 不是 int 数组,我认为你仍然可能会遇到可见性问题,因此我建议在消费者线程中在循环的开头放置一个读屏障。可能核心的缓存正在更新过程中,而您不会等待它-与读屏障所做的相反。 - auselen

6

“理论”是正确的,写入可以从写入屏障之后移动到之前。

您代码中的根本问题是线程2中没有任何同步。您在没有读取屏障的情况下读取了mSharedProducerIndex,因此谁知道您会得到什么值。无论您在线程1中做什么都不能解决这个问题。


抱歉,我过于简化了我的伪代码,稍后会进行编辑。感谢您的回答。 - sam-w
修改完成,希望能够澄清情况。干杯 :) - sam-w

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接