[英]Why would adding a delay improve data throughput in this multithreaded environment?
[英]Lazy loaded data in multithreaded environment
我有一个这样的结构:
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() {}
};
chunk
mSubLevels
为空,直到第一次访问。 第一次访问mSubLevels
我创建的阵列chunks
为mSubLevels
和填充其他成员。 但是因为有多个线程与chunks
工作,所以我使用mutex
执行此过程。 因此,新chunks
创建受到mutex
量的保护。 在此过程之后,将不会对此chunks
写操作,它们是只读数据,因此线程无需任何mutex
即可访问此chunks
。
确实,我有某种方法,在其中一种方法中,在第一次访问mSubLevels
我会检查该指针,如果该指针为null,我将通过mutex
创建所需的数据。 但是其他方法是只读的,并且我不更改structure
。 因此,我在此功能中不使用任何mutex
量。 (创建chunks
线程和读取它们的线程之间没有任何acquire/release
顺序)。
现在我可以使用常规数据类型,还是必须使用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 ...
读取器线程将看到写入器线程的更改,并且不会发生cache coherence
问题。 这句话是真的吗?
您的问题远不只是缓存一致性。 这是关于正确性。 您正在做的是双重检查锁定的情况 。
就一个线程可能看到mSubLevels
为空并分配一个新对象而言,这是有问题的。 发生这种情况时,另一个线程可能会同时访问mSubLevels
并看到它为空,并同时分配一个对象。 现在怎么办? 哪一个是分配给指针的“正确”对象。 您会泄漏一个物体,还是对另一个物体做什么? 怎么发现这种情况?
要解决此问题,您必须在检查值之前先锁定(即使用互斥锁),或者必须执行某种原子操作,以区分空对象与仍然无效的正在创建的对象和有效的对象(例如作为与(Chunk*)1
的原子比较交换,它基本上类似于微型自旋锁,只是您不旋转)。
一句话,是的,您至少必须为此使用原子操作,甚至是互斥体。 使用“普通”数据类型将不起作用。
对于只有阅读者而没有作家的所有其他事物,可以使用常规类型,它将很好地工作。
这里需要解决两个问题:
我建议只使用读写器互斥锁
基本思想是:
这种设计存在一些问题(特别是在初始化期间发生的争用),但是它的优点是非常简单。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.