10

我将描述的情况发生在 iPad 4 (ARMv7s) 上,使用 posix 库进行互斥锁定/解锁。不过,我在其他 ARMv7 设备上也看到过类似的事情(见下文),所以我认为任何解决方案都需要更全面地了解 ARMv7 的互斥锁和内存栅栏的行为。

场景的伪代码:

线程 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不是原子执行的,因此改为使用AtomicCompareAndSwapto assign 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

然而,这仍然失败,并让我质疑 amutex_unlock是否肯定会充当写栅栏?

我还阅读了 HP 的一篇文章,其中建议编译器可以将代码移入(但不能移出)crit_secs。因此,即使在上述更改之后,写入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写入可能会移动到屏障内,因此可能会被错误地重新排序。

是否建议向消费者添加一个互斥锁,只是作为一个读取屏障,或者是否有一个pragmaor 指令来禁用生产者上的乱序执行,比如EIEIO在 PPC 上?

4

2 回答 2

7

您的产品已同步,但您在消费时不进行任何同步(您还需要将内存与屏障同步)。因此,即使您为生产者提供了完美的内存屏障,但内存屏障也无助于消费者。

在您的代码中,您可能会受到编译器的排序、硬件排序的影响,甚至会受到mSharedProducerIndex运行线程#2 的其他核心上的陈旧值的影响。

您应该阅读Cortex™-A 系列程序员指南Chapter 11: Memory Ordering,尤其是.11.2.1 Memory barrier use example

我认为您的问题是您在消费者线程中获得了部分更新。问题是生产者的关键部分内部不是原子的,它可以重新排序。

not atomic我的意思是,如果您不是mSharedArray[TempProducerIndex++] = NewData;单词存储(NewData 具有 int 类型),则可以分几个步骤完成,其他核心可以将其视为部分更新。

reordering我的意思是互斥体提供进出障碍,但在关键部分不强加任何顺序。由于您在消费者方面没有任何特殊构造,因此您可以看到mSharedProducerIndex已更新,但仍可以看到对mSharedArray[mConsumerIndex]. Mutex 仅在执行离开临界区后保证内存可见性。

我相信这也解释了为什么当您OSMemoryBarrier();在临界区中添加它时它会起作用,因为这种方式 cpu 被迫将数据写入mSharedArray然后更新mConsumerIndex,并且当其他核心/线程看到mConsumerIndex我们知道mSharedArray由于障碍而被完全复制时。

假设您有多个生产者和一个消费者,我认为您的实现OSMemoryBarrier();是正确的。我不同意任何建议在消费者中设置内存屏障的评论,因为我相信这不会修复生产者内部关键部分中发生的部分更新或重新排序。

作为标题中您问题的答案,一般来说,afaik mutexes 在他们进入之前阅读障碍并在他们离开后写障碍。

于 2013-02-21T13:39:09.413 回答
6

“理论”是正确的,写可以​​从写栅栏之后移到它之前。

您的代码的根本问题是线程 2 中根本没有同步。您在mSharedProducerIndex没有读取障碍的情况下阅读,所以谁知道您会得到什么值。您在线程 1 中所做的任何事情都无法解决该问题。

于 2013-02-21T13:30:49.040 回答