我有这样的结构:
struct Chunk
{
private:
public:
Chunk* mParent;
Chunk* mSubLevels;
Int16 mDepth;
Int16 mIndex;
Reference<ValueType> mFirstItem;
Reference<ValueType> mLastItem;
public:
Chunk()
{
mSubLevels = nullptr;
mFirstItem = nullptr;
mLastItem = nullptr;
}
~Chunk() {}
};
mSubLevels
中的 chunk
在首次访问之前为空。首次访问mSubLevels
时,我会为chunks
创建一个mSubLevels
数组并填充其他成员。但由于多个线程与chunks
一起使用,我使用mutex
执行此过程。因此,新chunks
的创建受mutex
保护。在此过程之后,没有写入此chunks
并且它们是只读数据,因此线程可以访问此chunks
而没有任何mutex
。
确实,我有一些方法,在其中一个方法中,在第一次访问mSubLevels
时我检查这个指针,如果是null,我将通过mutex
创建所需的数据。但其他方法是只读的,我不会更改structure
。所以我在这个函数中不使用任何mutex
。 (创建acquire/release
的线程和读取它们的线程之间没有任何chunks
顺序。
现在我可以使用常规数据类型,还是必须使用atomic
类型?
编辑2:
为了创建数据,我使用double checked locking
:
(这是一个将创建新chunks
)
Chunk* lTargetChunk = ...;
if (!std::atomic_load(lTargetChunk->mSubLevels, std::memory_order_relaxed))
{
std::lock_guard lGaurd(mMutex);
if (!std::atomic_load(lTargetChunk->mSubLevels, std::memory_order_relaxed))
{
Chunk* lChunks = new Chunk[mLevelSizes[l]];
for (UINT32 i = 0; i < mLevelSizes[l]; ++i)
{
Chunk* lCurrentChunk = &lChunks[i];
lCurrentChunk->mParent = lTargetChunk;
lCurrentChunk->mDepth = lDepth - 1;
lCurrentChunk->mIndex = i;
st::atomic_store(lCurrentChunk->mSubLevels, (Chunk*)bcNULL, memory_order_relaxed);
}
bcAtomicOperation::bcAtomicStore(lTargetChunk->mSubLevels, lChunks, std::memory_order_release);
}
}
暂时想象一下,我不使用原子操作mSubLevels
。
我还有其他方法只会在没有任何'互斥锁'的情况下读取此chunks
:
bcInline Chunk* _getSuccessorChunk(const Chunk* pChunk)
{
// If pChunk->mSubLevels isn't null do this operation.
const Chunk* lChunk = &pChunk->mSubLevels[0];
Chunk* lNextChunk;
if (lChunk->mIndex != mLevelSizes[lChunk->mDepth] - 1)
{
lNextChunk = lChunk + 1;
return lNextChunk;
}
else ...
正如您所见,我可以访问mSubLevels
,mIndex
和其他一些内容。在这个函数中我不使用任何'互斥',所以如果编写器线程没有将它的缓存刷新到主内存,任何运行此函数的线程都不会看到受影响的更改。如果我在此函数中使用mMutex
,我认为问题将得到解决。 (编写器线程和读取器线程将通过互斥锁中的原子操作进行同步)现在如果我在第一个函数中使用原子操作mSubLevels
(正如我已经写过)并使用'acquire'来加载第二个函数:
bcInline Chunk* _getSuccessorChunk(const Chunk* pChunk)
{
// If pChunk->mSubLevels isn't null do this operation.
const Chunk* lChunk = &std::atomic_load(pChunk->mSubLevels, std::memory_order_acquire)[0];
Chunk* lNextChunk;
if (lChunk->mIndex != mLevelSizes[lChunk->mDepth] - 1)
{
lNextChunk = lChunk + 1;
return lNextChunk;
}
else ...
Reader线程将看到来自编写器线程的更改,并且不会发生cache coherence
问题。这句话是真的吗?
答案 0 :(得分:3)
你的问题远远超过缓存一致性。这是关于正确性的。你正在做的是double checked locking的案例。
只要一个线程看到mSubLevels
为空并分配新对象,就会出现问题。在发生这种情况时,另一个线程可以同时访问mSubLevels
并看到它为null,并分配一个对象。现在怎么办?哪一个是分配给指针的“正确”对象。你会泄漏一个物体,或者你用另一个物体做什么?如何检测这种状况?
要解决此问题,您必须在检查值之前进行锁定(即使用互斥锁),或者您必须执行某种原子操作,以便区分空对象和仍然无效的对象正在创建的对象和一个有效的对象(例如与(Chunk*)1
的原子比较交换,它基本上就像一个微型自旋锁,除了你没有旋转)。
所以总之,是的,你必须至少使用原子操作,甚至是互斥。使用“普通”数据类型将无效。
对于其他只有读者而没有作家的人来说,你可以使用常规类型,它可以正常工作。
答案 1 :(得分:1)
这里有两个问题需要克服:
我建议只使用读写器互斥锁
基本理念是:
此设计存在一些问题(特别是初始化期间发生的争用),但它具有简单易用的优点。