繁体   English   中英

在多线程环境中延迟加载数据

[英]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我创建的阵列chunksmSubLevels和填充其他成员。 但是因为有多个线程与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 ...

如您所见,我可以访问mSubLevelsmIndex等。 在此函数中,我不使用任何“互斥体”,因此,如果编写器线程未将其缓存刷新到主内存,则将运行此函数的任何线程都不会看到受影响的更改。 如果我在此功能中使用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的原子比较交换,它基本上类似于微型自旋锁,只是您不旋转)。

一句话,是的,您至少必须为此使用原子操作,甚至是互斥体。 使用“普通”数据类型将不起作用。

对于只有阅读者而没有作家的所有其他事物,可以使用常规类型,它将很好地工作。

这里需要解决两个问题:

  1. 很显然,如果不创建数组,您将无法承受读取的费用
  2. 出于效率考虑,您可能不想多次创建阵列

我建议只使用读写器互斥锁

基本思想是:

  • 锁定阅读器模式
  • 检查数据是否准备好
  • 如果尚未准备就绪,请将锁升级到写入器模式
  • 检查数据是否准备好(可能是其他编写者准备的),如果没有准备好
  • 在写入器模式下释放锁定(在读取器模式下保持锁定)
  • 用数据做事
  • 在阅读器模式下释放锁

这种设计存在一些问题(特别是在初始化期间发生的争用),但是它的优点是非常简单。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM